| package com.android.launcher3.allapps; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.util.Log; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.animation.AccelerateInterpolator; |
| import android.view.animation.DecelerateInterpolator; |
| import android.view.animation.Interpolator; |
| |
| import com.android.launcher3.CellLayout; |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.Hotseat; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.LauncherAnimUtils; |
| import com.android.launcher3.PagedView; |
| import com.android.launcher3.Workspace; |
| import com.android.launcher3.util.TouchController; |
| |
| /** |
| * Handles AllApps view transition. |
| * 1) Slides all apps view using direct manipulation |
| * 2) When finger is released, animate to either top or bottom accordingly. |
| * |
| * Algorithm: |
| * If release velocity > THRES1, snap according to the direction of movement. |
| * If release velocity < THRES1, snap according to either top or bottom depending on whether it's |
| * closer to top or closer to the page indicator. |
| */ |
| public class AllAppsTransitionController implements TouchController, VerticalPullDetector.Listener { |
| |
| private static final String TAG = "AllAppsTrans"; |
| private static final boolean DBG = false; |
| |
| private final Interpolator mAccelInterpolator = new AccelerateInterpolator(2f); |
| private final Interpolator mDecelInterpolator = new DecelerateInterpolator(1f); |
| |
| private static final float ANIMATION_DURATION = 2000; |
| public static final float ALL_APPS_FINAL_ALPHA = .8f; |
| |
| private static final float PARALLAX_COEFFICIENT = .125f; |
| |
| private AllAppsContainerView mAppsView; |
| private Workspace mWorkspace; |
| private Hotseat mHotseat; |
| private float mHotseatBackgroundAlpha; |
| |
| private float mStatusBarHeight; |
| |
| private final Launcher mLauncher; |
| private final VerticalPullDetector mDetector; |
| |
| // Animation in this class is controlled by a single variable {@link mProgressTransY}. |
| // Visually, it represents top y coordinate of the all apps container. Using the |
| // {@link mTranslation} as the denominator, this fraction value ranges in [0, 1]. |
| private float mProgressTransY; // numerator |
| private float mTranslation = -1; // denominator |
| |
| private static final float RECATCH_REJECTION_FRACTION = .0875f; |
| |
| // Used in landscape. |
| private static final float BAZEL_PULL_UP_HEIGHT = 60; |
| |
| private long mAnimationDuration; |
| private float mCurY; |
| |
| private AnimatorSet mCurrentAnimation; |
| private boolean mNoIntercept; |
| |
| private boolean mLightStatusBar; |
| |
| // At the end of scroll settling, this class also sets the state of the launcher. |
| // If it's already set,do not call the #mLauncher.setXXX method. |
| private boolean mStateAlreadyChanged; |
| |
| public AllAppsTransitionController(Launcher launcher) { |
| mLauncher = launcher; |
| mDetector = new VerticalPullDetector(launcher); |
| mDetector.setListener(this); |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| if (ev.getAction() == MotionEvent.ACTION_DOWN) { |
| mNoIntercept = false; |
| if (mLauncher.getWorkspace().isInOverviewMode() || mLauncher.isWidgetsViewVisible()) { |
| mNoIntercept = true; |
| } else if (mLauncher.isAllAppsVisible() && |
| !mAppsView.shouldContainerScroll(ev.getX(), ev.getY())) { |
| mNoIntercept = true; |
| } else if (!mLauncher.isAllAppsVisible() && !shouldPossiblyIntercept(ev)) { |
| mNoIntercept = true; |
| } else { |
| mDetector.setDetectableScrollConditions(mLauncher.isAllAppsVisible() /* down */, |
| isInDisallowRecatchTopZone(), isInDisallowRecatchBottomZone()); |
| } |
| } |
| if (mNoIntercept) { |
| return false; |
| } |
| mDetector.onTouchEvent(ev); |
| return mDetector.shouldIntercept(); |
| } |
| |
| private boolean shouldPossiblyIntercept(MotionEvent ev) { |
| DeviceProfile grid = mLauncher.getDeviceProfile(); |
| if (mDetector.isRestingState()) { |
| if (mLauncher.getDragLayer().isEventOverHotseat(ev) && !grid.isLandscape) { |
| return true; |
| } |
| if (ev.getY() > mLauncher.getDeviceProfile().heightPx - BAZEL_PULL_UP_HEIGHT && |
| grid.isLandscape) { |
| return true; |
| } |
| return false; |
| } else { |
| return true; |
| } |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| return mDetector.onTouchEvent(ev); |
| } |
| |
| private boolean isInDisallowRecatchTopZone() { |
| return mProgressTransY / mTranslation < RECATCH_REJECTION_FRACTION; |
| } |
| |
| private boolean isInDisallowRecatchBottomZone() { |
| return mProgressTransY / mTranslation > 1 - RECATCH_REJECTION_FRACTION; |
| } |
| |
| @Override |
| public void onScrollStart(boolean start) { |
| cancelAnimation(); |
| mCurrentAnimation = LauncherAnimUtils.createAnimatorSet(); |
| preparePull(start); |
| } |
| |
| /** |
| * @param start {@code true} if start of new drag. |
| */ |
| public void preparePull(boolean start) { |
| // Initialize values that should not change until #onScrollEnd |
| mCurY = mAppsView.getTranslationY(); |
| mStatusBarHeight = mLauncher.getDragLayer().getInsets().top; |
| mHotseat.setVisibility(View.VISIBLE); |
| mHotseat.bringToFront(); |
| if (start) { |
| if (!mLauncher.isAllAppsVisible()) { |
| mLauncher.tryAndUpdatePredictedApps(); |
| mHotseatBackgroundAlpha = mHotseat.getBackground().getAlpha() / 255f; |
| mHotseat.setBackgroundTransparent(true /* transparent */); |
| mAppsView.setVisibility(View.VISIBLE); |
| mAppsView.getContentView().setVisibility(View.VISIBLE); |
| mAppsView.getContentView().setBackground(null); |
| mAppsView.getRevealView().setVisibility(View.VISIBLE); |
| mAppsView.getRevealView().setAlpha(mHotseatBackgroundAlpha); |
| |
| DeviceProfile grid= mLauncher.getDeviceProfile(); |
| if (!grid.isLandscape) { |
| mTranslation = mHotseat.getTop(); |
| } else { |
| mTranslation = mHotseat.getBottom(); |
| } |
| setProgress(mTranslation); |
| } else { |
| // TODO: get rid of this workaround to override state change by workspace transition |
| mWorkspace.onLauncherTransitionPrepare(mLauncher, false, false); |
| View child = ((CellLayout) mWorkspace.getChildAt(mWorkspace.getNextPage())) |
| .getShortcutsAndWidgets(); |
| child.setVisibility(View.VISIBLE); |
| child.setAlpha(1f); |
| } |
| } |
| } |
| |
| private void updateLightStatusBar(float progress) { |
| boolean enable = (progress < mStatusBarHeight / 2); |
| // Already set correctly |
| if (mLightStatusBar == enable) { |
| return; |
| } |
| int systemUiFlags = mLauncher.getWindow().getDecorView().getSystemUiVisibility(); |
| if (enable) { |
| mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags |
| | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); |
| |
| } else { |
| mLauncher.getWindow().getDecorView().setSystemUiVisibility(systemUiFlags |
| & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); |
| |
| } |
| mLightStatusBar = enable; |
| } |
| |
| @Override |
| public boolean onScroll(float displacement, float velocity) { |
| if (mAppsView == null) { |
| return false; // early termination. |
| } |
| if (0 <= mCurY + displacement && mCurY + displacement < mTranslation) { |
| setProgress(mCurY + displacement); |
| } |
| return true; |
| } |
| |
| /** |
| * @param progress y value of the border between hotseat and all apps |
| */ |
| public void setProgress(float progress) { |
| updateLightStatusBar(progress); |
| mProgressTransY = progress; |
| float alpha = calcAlphaAllApps(progress); |
| float workspaceHotseatAlpha = 1 - alpha; |
| |
| mAppsView.getRevealView().setAlpha(Math.min(ALL_APPS_FINAL_ALPHA, Math.max(mHotseatBackgroundAlpha, |
| mDecelInterpolator.getInterpolation(alpha)))); |
| mAppsView.getContentView().setAlpha(alpha); |
| mAppsView.setTranslationY(progress); |
| mWorkspace.setWorkspaceTranslation(View.TRANSLATION_Y, |
| PARALLAX_COEFFICIENT *(-mTranslation + progress), |
| mAccelInterpolator.getInterpolation(workspaceHotseatAlpha)); |
| mWorkspace.setHotseatTranslation(View.TRANSLATION_Y, -mTranslation + progress, |
| mAccelInterpolator.getInterpolation(workspaceHotseatAlpha)); |
| } |
| |
| public float getProgress() { |
| return mProgressTransY; |
| } |
| |
| private float calcAlphaAllApps(float progress) { |
| return ((mTranslation - progress)/mTranslation); |
| } |
| |
| @Override |
| public void onScrollEnd(float velocity, boolean fling) { |
| if (mAppsView == null) { |
| return; // early termination. |
| } |
| |
| if (fling) { |
| if (velocity < 0) { |
| calculateDuration(velocity, mAppsView.getTranslationY()); |
| animateToAllApps(mCurrentAnimation, mAnimationDuration); |
| } else { |
| calculateDuration(velocity, Math.abs(mTranslation - mAppsView.getTranslationY())); |
| animateToWorkspace(mCurrentAnimation, mAnimationDuration); |
| } |
| // snap to top or bottom using the release velocity |
| } else { |
| if (mAppsView.getTranslationY() > mTranslation / 2) { |
| calculateDuration(velocity, Math.abs(mTranslation - mAppsView.getTranslationY())); |
| animateToWorkspace(mCurrentAnimation, mAnimationDuration); |
| } else { |
| calculateDuration(velocity, Math.abs(mAppsView.getTranslationY())); |
| animateToAllApps(mCurrentAnimation, mAnimationDuration); |
| } |
| } |
| mCurrentAnimation.start(); |
| } |
| |
| private void calculateDuration(float velocity, float disp) { |
| // TODO: make these values constants after tuning. |
| float velocityDivisor = Math.max(1.5f, Math.abs(0.5f * velocity)); |
| float travelDistance = Math.max(0.2f, disp / mTranslation); |
| mAnimationDuration = (long) Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance); |
| if (DBG) { |
| Log.d(TAG, String.format("calculateDuration=%d, v=%f, d=%f", mAnimationDuration, velocity, disp)); |
| } |
| } |
| |
| public void animateToAllApps(AnimatorSet animationOut, long duration) { |
| if (animationOut == null){ |
| return; |
| } |
| if (mDetector.isRestingState()) { |
| preparePull(true); |
| mAnimationDuration = duration; |
| mStateAlreadyChanged = true; |
| } |
| mCurY = mAppsView.getTranslationY(); |
| final float fromAllAppsTop = mAppsView.getTranslationY(); |
| final float toAllAppsTop = 0; |
| |
| ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", |
| fromAllAppsTop, toAllAppsTop); |
| driftAndAlpha.setDuration(mAnimationDuration); |
| driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator()); |
| animationOut.play(driftAndAlpha); |
| |
| animationOut.addListener(new AnimatorListenerAdapter() { |
| boolean canceled = false; |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| canceled = true; |
| } |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (canceled) { |
| return; |
| } else { |
| finishPullUp(); |
| cleanUpAnimation(); |
| mDetector.finishedScrolling(); |
| } |
| }}); |
| mCurrentAnimation = animationOut; |
| } |
| |
| public void animateToWorkspace(AnimatorSet animationOut, long duration) { |
| if (animationOut == null){ |
| return; |
| } |
| if(mDetector.isRestingState()) { |
| preparePull(true); |
| mAnimationDuration = duration; |
| mStateAlreadyChanged = true; |
| } |
| final float fromAllAppsTop = mAppsView.getTranslationY(); |
| final float toAllAppsTop = mTranslation; |
| |
| ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress", |
| fromAllAppsTop, toAllAppsTop); |
| driftAndAlpha.setDuration(mAnimationDuration); |
| driftAndAlpha.setInterpolator(new PagedView.ScrollInterpolator()); |
| animationOut.play(driftAndAlpha); |
| |
| animationOut.addListener(new AnimatorListenerAdapter() { |
| boolean canceled = false; |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| canceled = true; |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| if (canceled) { |
| return; |
| } else { |
| finishPullDown(); |
| cleanUpAnimation(); |
| mDetector.finishedScrolling(); |
| } |
| }}); |
| mCurrentAnimation = animationOut; |
| } |
| |
| private void finishPullUp() { |
| mHotseat.setVisibility(View.INVISIBLE); |
| setProgress(0f); |
| if (!mStateAlreadyChanged) { |
| mLauncher.showAppsView(false /* animated */, true /* resetListToTop */, |
| false /* updatePredictedApps */, false /* focusSearchBar */); |
| } |
| mStateAlreadyChanged = false; |
| } |
| |
| public void finishPullDown() { |
| if (mHotseat.getBackground() != null) { |
| return; |
| } |
| mAppsView.setVisibility(View.INVISIBLE); |
| mHotseat.setBackgroundTransparent(false /* transparent */); |
| mHotseat.setVisibility(View.VISIBLE); |
| setProgress(mTranslation); |
| if (!mStateAlreadyChanged) { |
| mLauncher.showWorkspace(false); |
| } |
| mStateAlreadyChanged = false; |
| } |
| |
| private void cancelAnimation() { |
| if (mCurrentAnimation != null) { |
| mCurrentAnimation.cancel(); |
| mCurrentAnimation = null; |
| } |
| } |
| |
| private void cleanUpAnimation() { |
| mCurrentAnimation = null; |
| } |
| |
| public void setupViews(AllAppsContainerView appsView, Hotseat hotseat, Workspace workspace) { |
| mAppsView = appsView; |
| mHotseat = hotseat; |
| mWorkspace = workspace; |
| } |
| } |