| /* |
| * Copyright (C) 2021 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.taskbar; |
| |
| import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; |
| import static com.android.launcher3.Utilities.squaredHypot; |
| import static com.android.launcher3.anim.Interpolators.LINEAR; |
| import static com.android.quickstep.AnimatedFloat.VALUE; |
| |
| import android.graphics.Rect; |
| import android.util.FloatProperty; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewTreeObserver; |
| import android.view.ViewTreeObserver.OnPreDrawListener; |
| |
| import com.android.launcher3.BubbleTextView; |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.LauncherAppState; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.anim.AnimatorPlaybackController; |
| import com.android.launcher3.anim.PendingAnimation; |
| import com.android.launcher3.folder.FolderIcon; |
| import com.android.launcher3.model.data.ItemInfo; |
| import com.android.launcher3.util.MultiValueAlpha; |
| import com.android.quickstep.AnimatedFloat; |
| |
| /** |
| * Handles properties/data collection, then passes the results to TaskbarView to render. |
| */ |
| public class TaskbarViewController { |
| private static final Runnable NO_OP = () -> { }; |
| |
| public static final int ALPHA_INDEX_HOME = 0; |
| public static final int ALPHA_INDEX_IME = 1; |
| public static final int ALPHA_INDEX_KEYGUARD = 2; |
| public static final int ALPHA_INDEX_STASH = 3; |
| public static final int ALPHA_INDEX_RECENTS_DISABLED = 4; |
| public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 5; |
| private static final int NUM_ALPHA_CHANNELS = 6; |
| |
| private final TaskbarActivityContext mActivity; |
| private final TaskbarView mTaskbarView; |
| private final MultiValueAlpha mTaskbarIconAlpha; |
| private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale); |
| private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat( |
| this::updateTranslationY); |
| private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat( |
| this::updateTranslationY); |
| private AnimatedFloat mTaskbarNavButtonTranslationY; |
| |
| private final TaskbarModelCallbacks mModelCallbacks; |
| |
| // Initialized in init. |
| private TaskbarControllers mControllers; |
| |
| // Animation to align icons with Launcher, created lazily. This allows the controller to be |
| // active only during the animation and does not need to worry about layout changes. |
| private AnimatorPlaybackController mIconAlignControllerLazy = null; |
| private Runnable mOnControllerPreCreateCallback = NO_OP; |
| |
| public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) { |
| mActivity = activity; |
| mTaskbarView = taskbarView; |
| mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, NUM_ALPHA_CHANNELS); |
| mTaskbarIconAlpha.setUpdateVisibility(true); |
| mModelCallbacks = new TaskbarModelCallbacks(activity, mTaskbarView); |
| } |
| |
| public void init(TaskbarControllers controllers) { |
| mControllers = controllers; |
| mTaskbarView.init(new TaskbarViewCallbacks()); |
| mTaskbarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize; |
| |
| mTaskbarIconScaleForStash.updateValue(1f); |
| |
| mModelCallbacks.init(controllers); |
| if (mActivity.isUserSetupComplete()) { |
| // Only load the callbacks if user setup is completed |
| LauncherAppState.getInstance(mActivity).getModel().addCallbacksAndLoad(mModelCallbacks); |
| } |
| mTaskbarNavButtonTranslationY = |
| controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY(); |
| } |
| |
| public void onDestroy() { |
| LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks); |
| } |
| |
| public boolean areIconsVisible() { |
| return mTaskbarView.areIconsVisible(); |
| } |
| |
| public MultiValueAlpha getTaskbarIconAlpha() { |
| return mTaskbarIconAlpha; |
| } |
| |
| /** |
| * Should be called when the IME visibility changes, so we can make Taskbar not steal touches. |
| */ |
| public void setImeIsVisible(boolean isImeVisible) { |
| mTaskbarView.setTouchesEnabled(!isImeVisible); |
| } |
| |
| /** |
| * Should be called when the recents button is disabled, so we can hide taskbar icons as well. |
| */ |
| public void setRecentsButtonDisabled(boolean isDisabled) { |
| // TODO: check TaskbarStashController#supportsStashing(), to stash instead of setting alpha. |
| mTaskbarIconAlpha.getProperty(ALPHA_INDEX_RECENTS_DISABLED).setValue(isDisabled ? 0 : 1); |
| } |
| |
| /** |
| * Sets OnClickListener and OnLongClickListener for the given view. |
| */ |
| public void setClickAndLongClickListenersForIcon(View icon) { |
| mTaskbarView.setClickAndLongClickListenersForIcon(icon); |
| } |
| |
| /** |
| * Adds one time pre draw listener to the taskbar view, it is called before |
| * drawing a frame and invoked only once |
| * @param listener callback that will be invoked before drawing the next frame |
| */ |
| public void addOneTimePreDrawListener(Runnable listener) { |
| mTaskbarView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { |
| @Override |
| public boolean onPreDraw() { |
| final ViewTreeObserver viewTreeObserver = mTaskbarView.getViewTreeObserver(); |
| if (viewTreeObserver.isAlive()) { |
| listener.run(); |
| viewTreeObserver.removeOnPreDrawListener(this); |
| } |
| return true; |
| } |
| }); |
| } |
| |
| public Rect getIconLayoutBounds() { |
| return mTaskbarView.getIconLayoutBounds(); |
| } |
| |
| public View[] getIconViews() { |
| return mTaskbarView.getIconViews(); |
| } |
| |
| public AnimatedFloat getTaskbarIconScaleForStash() { |
| return mTaskbarIconScaleForStash; |
| } |
| |
| public AnimatedFloat getTaskbarIconTranslationYForStash() { |
| return mTaskbarIconTranslationYForStash; |
| } |
| |
| /** |
| * Applies scale properties for the entire TaskbarView (rather than individual icons). |
| */ |
| private void updateScale() { |
| float scale = mTaskbarIconScaleForStash.value; |
| mTaskbarView.setScaleX(scale); |
| mTaskbarView.setScaleY(scale); |
| } |
| |
| private void updateTranslationY() { |
| mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value |
| + mTaskbarIconTranslationYForStash.value); |
| } |
| |
| /** |
| * Sets the taskbar icon alignment relative to Launcher hotseat icons |
| * @param alignmentRatio [0, 1] |
| * 0 => not aligned |
| * 1 => fully aligned |
| */ |
| public void setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp) { |
| if (mIconAlignControllerLazy == null) { |
| mIconAlignControllerLazy = createIconAlignmentController(launcherDp); |
| } |
| mIconAlignControllerLazy.setPlayFraction(alignmentRatio); |
| if (alignmentRatio <= 0 || alignmentRatio >= 1) { |
| // Cleanup lazy controller so that it is created again in next animation |
| mIconAlignControllerLazy = null; |
| } |
| } |
| |
| /** |
| * Creates an animation for aligning the taskbar icons with the provided Launcher device profile |
| */ |
| private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) { |
| mOnControllerPreCreateCallback.run(); |
| PendingAnimation setter = new PendingAnimation(100); |
| Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity); |
| float scaleUp = ((float) launcherDp.iconSizePx) / mActivity.getDeviceProfile().iconSizePx; |
| int hotseatCellSize = |
| (launcherDp.availableWidthPx - hotseatPadding.left - hotseatPadding.right) |
| / launcherDp.numShownHotseatIcons; |
| |
| int offsetY = launcherDp.getTaskbarOffsetY(); |
| setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, LINEAR); |
| setter.setFloat(mTaskbarNavButtonTranslationY, VALUE, -offsetY, LINEAR); |
| |
| int collapsedHeight = mActivity.getDefaultTaskbarWindowHeight(); |
| int expandedHeight = Math.max(collapsedHeight, |
| mActivity.getDeviceProfile().taskbarSize + offsetY); |
| setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowHeight( |
| anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight)); |
| |
| int count = mTaskbarView.getChildCount(); |
| for (int i = 0; i < count; i++) { |
| View child = mTaskbarView.getChildAt(i); |
| ItemInfo info = (ItemInfo) child.getTag(); |
| setter.setFloat(child, SCALE_PROPERTY, scaleUp, LINEAR); |
| |
| float childCenter = (child.getLeft() + child.getRight()) / 2; |
| float hotseatIconCenter = hotseatPadding.left + hotseatCellSize * info.screenId |
| + hotseatCellSize / 2; |
| setter.setFloat(child, ICON_TRANSLATE_X, hotseatIconCenter - childCenter, LINEAR); |
| } |
| |
| AnimatorPlaybackController controller = setter.createPlaybackController(); |
| mOnControllerPreCreateCallback = () -> controller.setPlayFraction(0); |
| return controller; |
| } |
| |
| public void onRotationChanged(DeviceProfile deviceProfile) { |
| if (areIconsVisible()) { |
| // We only translate on rotation when on home |
| return; |
| } |
| mTaskbarNavButtonTranslationY.updateValue(-deviceProfile.getTaskbarOffsetY()); |
| } |
| |
| /** |
| * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's |
| * touch bounds. |
| */ |
| public boolean isEventOverAnyItem(MotionEvent ev) { |
| return mTaskbarView.isEventOverAnyItem(ev); |
| } |
| |
| /** |
| * Callbacks for {@link TaskbarView} to interact with its controller. |
| */ |
| public class TaskbarViewCallbacks { |
| private final float mSquaredTouchSlop = Utilities.squaredTouchSlop(mActivity); |
| |
| private float mDownX, mDownY; |
| private boolean mCanceledStashHint; |
| |
| public View.OnClickListener getIconOnClickListener() { |
| return mActivity::onTaskbarIconClicked; |
| } |
| |
| public View.OnLongClickListener getIconOnLongClickListener() { |
| return mControllers.taskbarDragController::startDragOnLongClick; |
| } |
| |
| public View.OnLongClickListener getBackgroundOnLongClickListener() { |
| return view -> mControllers.taskbarStashController |
| .updateAndAnimateIsManuallyStashedInApp(true); |
| } |
| |
| /** |
| * Get the first chance to handle TaskbarView#onTouchEvent, and return whether we want to |
| * consume the touch so TaskbarView treats it as an ACTION_CANCEL. |
| */ |
| public boolean onTouchEvent(MotionEvent motionEvent) { |
| final float x = motionEvent.getRawX(); |
| final float y = motionEvent.getRawY(); |
| switch (motionEvent.getAction()) { |
| case MotionEvent.ACTION_DOWN: |
| mDownX = x; |
| mDownY = y; |
| mControllers.taskbarStashController.startStashHint(/* animateForward = */ true); |
| mCanceledStashHint = false; |
| break; |
| case MotionEvent.ACTION_MOVE: |
| if (!mCanceledStashHint |
| && squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) { |
| mControllers.taskbarStashController.startStashHint( |
| /* animateForward= */ false); |
| mCanceledStashHint = true; |
| return true; |
| } |
| break; |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| if (!mCanceledStashHint) { |
| mControllers.taskbarStashController.startStashHint( |
| /* animateForward= */ false); |
| } |
| break; |
| } |
| return false; |
| } |
| } |
| |
| public static final FloatProperty<View> ICON_TRANSLATE_X = |
| new FloatProperty<View>("taskbarAligmentTranslateX") { |
| |
| @Override |
| public void setValue(View view, float v) { |
| if (view instanceof BubbleTextView) { |
| ((BubbleTextView) view).setTranslationXForTaskbarAlignmentAnimation(v); |
| } else if (view instanceof FolderIcon) { |
| ((FolderIcon) view).setTranslationForTaskbarAlignmentAnimation(v); |
| } else { |
| view.setTranslationX(v); |
| } |
| } |
| |
| @Override |
| public Float get(View view) { |
| if (view instanceof BubbleTextView) { |
| return ((BubbleTextView) view) |
| .getTranslationXForTaskbarAlignmentAnimation(); |
| } else if (view instanceof FolderIcon) { |
| return ((FolderIcon) view).getTranslationXForTaskbarAlignmentAnimation(); |
| } |
| return view.getTranslationX(); |
| } |
| }; |
| } |