/* * 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.anim.Interpolators.ACCEL_2; import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; import static com.android.launcher3.anim.Interpolators.DEACCEL; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.Keyframe; import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.TimeInterpolator; import android.content.res.Resources; import android.text.TextUtils; import android.view.View; import com.android.launcher3.R; import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.uioverrides.PredictedAppIcon; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** Handles the Taskbar Education flow. */ public class TaskbarEduController { private static final long WAVE_ANIM_DELAY = 250; private static final long WAVE_ANIM_STAGGER = 50; private static final long WAVE_ANIM_EACH_ICON_DURATION = 633; private static final long WAVE_ANIM_SLOT_MACHINE_DURATION = 1085; // The fraction of each icon's animation at which we reach the top point of the wave. private static final float WAVE_ANIM_FRACTION_TOP = 0.4f; // The fraction of each icon's animation at which we reach the bottom, before overshooting. private static final float WAVE_ANIM_FRACTION_BOTTOM = 0.9f; private static final TimeInterpolator WAVE_ANIM_TO_TOP_INTERPOLATOR = FAST_OUT_SLOW_IN; private static final TimeInterpolator WAVE_ANIM_TO_BOTTOM_INTERPOLATOR = ACCEL_2; private static final TimeInterpolator WAVE_ANIM_OVERSHOOT_INTERPOLATOR = DEACCEL; private static final TimeInterpolator WAVE_ANIM_OVERSHOOT_RETURN_INTERPOLATOR = ACCEL_DEACCEL; private static final float WAVE_ANIM_ICON_SCALE = 1.2f; // How many icons to cycle through in the slot machine (+ the original icon at each end). private static final int WAVE_ANIM_SLOT_MACHINE_NUM_ICONS = 3; private final TaskbarActivityContext mActivity; private final float mWaveAnimTranslationY; private final float mWaveAnimTranslationYReturnOvershoot; // Initialized in init. TaskbarControllers mControllers; private TaskbarEduView mTaskbarEduView; private Animator mAnim; public TaskbarEduController(TaskbarActivityContext activity) { mActivity = activity; final Resources resources = activity.getResources(); mWaveAnimTranslationY = resources.getDimension(R.dimen.taskbar_edu_wave_anim_trans_y); mWaveAnimTranslationYReturnOvershoot = resources.getDimension( R.dimen.taskbar_edu_wave_anim_trans_y_return_overshoot); } public void init(TaskbarControllers controllers) { mControllers = controllers; } void showEdu() { mActivity.setTaskbarWindowFullscreen(true); mActivity.getDragLayer().post(() -> { mTaskbarEduView = (TaskbarEduView) mActivity.getLayoutInflater().inflate( R.layout.taskbar_edu, mActivity.getDragLayer(), false); mTaskbarEduView.init(new TaskbarEduCallbacks()); mTaskbarEduView.addOnCloseListener(() -> mTaskbarEduView = null); mTaskbarEduView.show(); startAnim(createWaveAnim()); }); } void hideEdu() { if (mTaskbarEduView != null) { mTaskbarEduView.close(true /* animate */); } } /** * Starts the given animation, ending the previous animation first if it's still playing. */ private void startAnim(Animator anim) { if (mAnim != null) { mAnim.end(); } mAnim = anim; mAnim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mAnim = null; } }); mAnim.start(); } /** * Creates a staggered "wave" animation where each icon translates and scales up in succession. */ private Animator createWaveAnim() { AnimatorSet waveAnim = new AnimatorSet(); View[] icons = mControllers.taskbarViewController.getIconViews(); for (int i = 0; i < icons.length; i++) { View icon = icons[i]; AnimatorSet iconAnim = new AnimatorSet(); Keyframe[] scaleKeyframes = new Keyframe[] { Keyframe.ofFloat(0, 1f), Keyframe.ofFloat(WAVE_ANIM_FRACTION_TOP, WAVE_ANIM_ICON_SCALE), Keyframe.ofFloat(WAVE_ANIM_FRACTION_BOTTOM, 1f), Keyframe.ofFloat(1f, 1f) }; scaleKeyframes[1].setInterpolator(WAVE_ANIM_TO_TOP_INTERPOLATOR); scaleKeyframes[2].setInterpolator(WAVE_ANIM_TO_BOTTOM_INTERPOLATOR); Keyframe[] translationYKeyframes = new Keyframe[] { Keyframe.ofFloat(0, 0f), Keyframe.ofFloat(WAVE_ANIM_FRACTION_TOP, -mWaveAnimTranslationY), Keyframe.ofFloat(WAVE_ANIM_FRACTION_BOTTOM, 0f), // Half of the remaining fraction overshoots, then the other half returns to 0. Keyframe.ofFloat( WAVE_ANIM_FRACTION_BOTTOM + (1 - WAVE_ANIM_FRACTION_BOTTOM) / 2f, mWaveAnimTranslationYReturnOvershoot), Keyframe.ofFloat(1f, 0f) }; translationYKeyframes[1].setInterpolator(WAVE_ANIM_TO_TOP_INTERPOLATOR); translationYKeyframes[2].setInterpolator(WAVE_ANIM_TO_BOTTOM_INTERPOLATOR); translationYKeyframes[3].setInterpolator(WAVE_ANIM_OVERSHOOT_INTERPOLATOR); translationYKeyframes[4].setInterpolator(WAVE_ANIM_OVERSHOOT_RETURN_INTERPOLATOR); iconAnim.play(ObjectAnimator.ofPropertyValuesHolder(icon, PropertyValuesHolder.ofKeyframe(SCALE_PROPERTY, scaleKeyframes)) .setDuration(WAVE_ANIM_EACH_ICON_DURATION)); iconAnim.play(ObjectAnimator.ofPropertyValuesHolder(icon, PropertyValuesHolder.ofKeyframe(View.TRANSLATION_Y, translationYKeyframes)) .setDuration(WAVE_ANIM_EACH_ICON_DURATION)); if (icon instanceof PredictedAppIcon) { // Play slot machine animation through random icons from AllAppsList. PredictedAppIcon predictedAppIcon = (PredictedAppIcon) icon; ItemInfo itemInfo = (ItemInfo) icon.getTag(); List iconsToAnimate = mControllers.uiController.getAppIconsForEdu() .filter(appInfo -> !TextUtils.equals(appInfo.title, itemInfo.title)) .map(appInfo -> appInfo.bitmap) .filter(bitmap -> !bitmap.isNullOrLowRes()) .collect(Collectors.toList()); // Pick n icons at random. Collections.shuffle(iconsToAnimate); if (iconsToAnimate.size() > WAVE_ANIM_SLOT_MACHINE_NUM_ICONS) { iconsToAnimate = iconsToAnimate.subList(0, WAVE_ANIM_SLOT_MACHINE_NUM_ICONS); } Animator slotMachineAnim = predictedAppIcon.createSlotMachineAnim(iconsToAnimate); if (slotMachineAnim != null) { iconAnim.play(slotMachineAnim.setDuration(WAVE_ANIM_SLOT_MACHINE_DURATION)); } } iconAnim.setStartDelay(WAVE_ANIM_STAGGER * i); waveAnim.play(iconAnim); } waveAnim.setStartDelay(WAVE_ANIM_DELAY); return waveAnim; } /** * Callbacks for {@link TaskbarEduView} to interact with its controller. */ class TaskbarEduCallbacks { void onPageChanged(int currentPage, int pageCount) { if (currentPage == 0) { mTaskbarEduView.updateStartButton(R.string.taskbar_edu_close, v -> mTaskbarEduView.close(true /* animate */)); } else { mTaskbarEduView.updateStartButton(R.string.taskbar_edu_previous, v -> mTaskbarEduView.snapToPage(currentPage - 1)); } if (currentPage == pageCount - 1) { mTaskbarEduView.updateEndButton(R.string.taskbar_edu_done, v -> mTaskbarEduView.close(true /* animate */)); } else { mTaskbarEduView.updateEndButton(R.string.taskbar_edu_next, v -> mTaskbarEduView.snapToPage(currentPage + 1)); } } } }