diff options
Diffstat (limited to 'src/com/android')
6 files changed, 374 insertions, 5 deletions
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index d9ee2c55a..a399d748a 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -20,6 +20,8 @@ import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.InsetDrawable; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.Selection; import android.text.SpannableStringBuilder; @@ -42,6 +44,7 @@ import com.android.launcher3.Launcher; import com.android.launcher3.PromiseAppInfo; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.anim.SpringAnimationHandler; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragOptions; @@ -63,7 +66,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc private final Launcher mLauncher; private final AlphabeticalAppsList mApps; private final AllAppsGridAdapter mAdapter; - private final RecyclerView.LayoutManager mLayoutManager; + private final LinearLayoutManager mLayoutManager; private AllAppsRecyclerView mAppsRecyclerView; private SearchUiManager mSearchUiManager; @@ -74,6 +77,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc private int mNumAppsPerRow; private int mNumPredictedAppsPerRow; + private SpringAnimationHandler mSpringAnimationHandler; + public AllAppsContainerView(Context context) { this(context, null); } @@ -87,7 +92,9 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mLauncher = Launcher.getLauncher(context); mApps = new AlphabeticalAppsList(context); - mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this); + mSpringAnimationHandler = new SpringAnimationHandler(SpringAnimationHandler.Y_DIRECTION); + mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this, + mSpringAnimationHandler); mApps.setAdapter(mAdapter); mLayoutManager = mAdapter.getLayoutManager(); mSearchQueryBuilder = new SpannableStringBuilder(); @@ -227,6 +234,10 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc mAppsRecyclerView.setLayoutManager(mLayoutManager); mAppsRecyclerView.setAdapter(mAdapter); mAppsRecyclerView.setHasFixedSize(true); + if (FeatureFlags.LAUNCHER3_PHYSICS) { + mAppsRecyclerView.setSpringAnimationHandler(mSpringAnimationHandler); + mAppsRecyclerView.addOnScrollListener(new SpringMotionOnScrollListener()); + } mSearchContainer = findViewById(R.id.search_container); mSearchUiManager = (SearchUiManager) mSearchContainer; @@ -404,4 +415,36 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc } } } + + public SpringAnimationHandler getSpringAnimationHandler() { + return mSpringAnimationHandler; + } + + public class SpringMotionOnScrollListener extends RecyclerView.OnScrollListener { + + private int mScrollState = RecyclerView.SCROLL_STATE_IDLE; + + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + if (mScrollState == RecyclerView.SCROLL_STATE_DRAGGING) { + mSpringAnimationHandler.skipToEnd(); + return; + } + + int first = mLayoutManager.findFirstVisibleItemPosition(); + int last = mLayoutManager.findLastVisibleItemPosition(); + + // We only show the spring animation when at the top or bottom, so we wait until the + // first or last row is visible to ensure that all animations run in sync. + if (first == 0 || last >= mAdapter.getItemCount() - mAdapter.getNumAppsPerRow()) { + mSpringAnimationHandler.animateToFinalPosition(0); + } + } + + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + mScrollState = newState; + } + } } diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index cfd04e2e0..d3d23ca24 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -19,6 +19,7 @@ import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Point; +import android.support.animation.SpringAnimation; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.support.v4.view.accessibility.AccessibilityRecordCompat; @@ -38,6 +39,8 @@ import com.android.launcher3.BubbleTextView; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem; +import com.android.launcher3.anim.SpringAnimationHandler; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.discovery.AppDiscoveryAppInfo; import com.android.launcher3.discovery.AppDiscoveryItemView; import com.android.launcher3.util.PackageManagerHelper; @@ -80,6 +83,8 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. | VIEW_TYPE_PREDICTION_ICON; public static final int VIEW_TYPE_MASK_CONTENT = VIEW_TYPE_MASK_ICON | VIEW_TYPE_DISCOVERY_ITEM; + public static final int VIEW_TYPE_MASK_HAS_SPRINGS = VIEW_TYPE_MASK_ICON + | VIEW_TYPE_PREDICTION_DIVIDER; public interface BindViewCallback { @@ -90,6 +95,12 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. * ViewHolder for each icon. */ public static class ViewHolder extends RecyclerView.ViewHolder { + + /** + * Springs used for items where isViewType(viewType, VIEW_TYPE_MASK_HAS_SPRINGS) is true. + */ + private SpringAnimation spring; + public ViewHolder(View v) { super(v); } @@ -202,8 +213,11 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. // The intent to send off to the market app, updated each time the search query changes. private Intent mMarketSearchIntent; + private SpringAnimationHandler mSpringAnimationHandler; + public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps, View.OnClickListener - iconClickListener, View.OnLongClickListener iconLongClickListener) { + iconClickListener, View.OnLongClickListener iconLongClickListener, + SpringAnimationHandler springAnimationHandler) { Resources res = launcher.getResources(); mLauncher = launcher; mApps = apps; @@ -214,6 +228,7 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. mLayoutInflater = LayoutInflater.from(launcher); mIconClickListener = iconClickListener; mIconLongClickListener = iconLongClickListener; + mSpringAnimationHandler = springAnimationHandler; } public static boolean isDividerViewType(int viewType) { @@ -236,6 +251,10 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. mGridLayoutMgr.setSpanCount(appsPerRow); } + public int getNumAppsPerRow() { + return mAppsPerRow; + } + public void setIconFocusListener(OnFocusChangeListener focusListener) { mIconFocusListener = focusListener; } @@ -327,7 +346,6 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. AppInfo info = mApps.getAdapterItems().get(position).appInfo; BubbleTextView icon = (BubbleTextView) holder.itemView; icon.applyFromApplicationInfo(info); - icon.setAccessibilityDelegate(mLauncher.getAccessibilityDelegate()); break; case VIEW_TYPE_DISCOVERY_ITEM: AppDiscoveryAppInfo appDiscoveryAppInfo = (AppDiscoveryAppInfo) @@ -365,6 +383,23 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter<AllAppsGridAdapter. } @Override + public void onViewAttachedToWindow(ViewHolder holder) { + int type = holder.getItemViewType(); + if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) { + holder.spring = mSpringAnimationHandler.add(holder.itemView, + holder.getAdapterPosition(), mApps, mAppsPerRow, holder.spring); + } + } + + @Override + public void onViewDetachedFromWindow(ViewHolder holder) { + int type = holder.getItemViewType(); + if (FeatureFlags.LAUNCHER3_PHYSICS && isViewType(type, VIEW_TYPE_MASK_HAS_SPRINGS)) { + holder.spring = mSpringAnimationHandler.remove(holder.spring); + } + } + + @Override public boolean onFailedToRecycleView(ViewHolder holder) { // Always recycle and we will reset the view when it is bound return true; diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java index 16b2bd1fc..d76abccd3 100644 --- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java +++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java @@ -28,8 +28,8 @@ import android.view.View; import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Launcher; import com.android.launcher3.R; +import com.android.launcher3.anim.SpringAnimationHandler; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.graphics.DrawableFactory; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; @@ -53,6 +53,8 @@ public class AllAppsRecyclerView extends BaseRecyclerView { private AllAppsBackgroundDrawable mEmptySearchBackground; private int mEmptySearchBackgroundTopOffset; + private SpringAnimationHandler mSpringAnimationHandler; + public AllAppsRecyclerView(Context context) { this(context, null); } @@ -75,6 +77,18 @@ public class AllAppsRecyclerView extends BaseRecyclerView { R.dimen.all_apps_empty_search_bg_top_offset); } + public void setSpringAnimationHandler(SpringAnimationHandler springAnimationHandler) { + mSpringAnimationHandler = springAnimationHandler; + } + + @Override + public boolean onTouchEvent(MotionEvent e) { + if (FeatureFlags.LAUNCHER3_PHYSICS && mSpringAnimationHandler != null) { + mSpringAnimationHandler.addMovement(e); + } + return super.onTouchEvent(e); + } + /** * Sets the list of apps in this view, used to determine the fastscroll position. */ diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index 7c6ff5120..dd0d23810 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -22,6 +22,7 @@ import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; +import com.android.launcher3.anim.SpringAnimationHandler; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dynamicui.ExtractedColors; import com.android.launcher3.graphics.GradientView; @@ -99,6 +100,8 @@ public class AllAppsTransitionController implements TouchController, VerticalPul private GradientView mGradientView; private ScrimView mScrimView; + private SpringAnimationHandler mSpringAnimationHandler; + public AllAppsTransitionController(Launcher l) { mLauncher = l; mDetector = new VerticalPullDetector(l); @@ -161,6 +164,9 @@ public class AllAppsTransitionController implements TouchController, VerticalPul @Override public boolean onControllerTouchEvent(MotionEvent ev) { + if (hasSpringAnimationHandler()) { + mSpringAnimationHandler.addMovement(ev); + } return mDetector.onTouchEvent(ev); } @@ -179,6 +185,9 @@ public class AllAppsTransitionController implements TouchController, VerticalPul mCurrentAnimation = LauncherAnimUtils.createAnimatorSet(); mShiftStart = mAppsView.getTranslationY(); preparePull(start); + if (hasSpringAnimationHandler()) { + mSpringAnimationHandler.skipToEnd(); + } } @Override @@ -214,6 +223,9 @@ public class AllAppsTransitionController implements TouchController, VerticalPul mLauncher.showAppsView(true /* animated */, false /* updatePredictedApps */, false /* focusSearchBar */); + if (hasSpringAnimationHandler()) { + mSpringAnimationHandler.animateToFinalPosition(0); + } } else { calculateDuration(velocity, Math.abs(mShiftRange - mAppsView.getTranslationY())); mLauncher.showWorkspace(true); @@ -498,6 +510,9 @@ public class AllAppsTransitionController implements TouchController, VerticalPul public void finishPullUp() { mHotseat.setVisibility(View.INVISIBLE); + if (hasSpringAnimationHandler()) { + mSpringAnimationHandler.reset(); + } setProgress(0f); } @@ -506,6 +521,9 @@ public class AllAppsTransitionController implements TouchController, VerticalPul mHotseat.setBackgroundTransparent(false /* transparent */); mHotseat.setVisibility(View.VISIBLE); mAppsView.reset(); + if (hasSpringAnimationHandler()) { + mSpringAnimationHandler.reset(); + } setProgress(1f); } @@ -537,6 +555,11 @@ public class AllAppsTransitionController implements TouchController, VerticalPul mCaretController = new AllAppsCaretController( mWorkspace.getPageIndicator().getCaretDrawable(), mLauncher); mAppsView.getSearchUiManager().addOnScrollRangeChangeListener(this); + mSpringAnimationHandler = mAppsView.getSpringAnimationHandler(); + } + + private boolean hasSpringAnimationHandler() { + return FeatureFlags.LAUNCHER3_PHYSICS && mSpringAnimationHandler != null; } @Override diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 047441985..b84c6276a 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -228,6 +228,13 @@ public class AlphabeticalAppsList { } /** + * Returns the predicted apps. + */ + public List<AppInfo> getPredictedApps() { + return mPredictedApps; + } + + /** * Returns fast scroller sections of all the current filtered applications. */ public List<FastScrollSectionInfo> getFastScrollerSections() { diff --git a/src/com/android/launcher3/anim/SpringAnimationHandler.java b/src/com/android/launcher3/anim/SpringAnimationHandler.java new file mode 100644 index 000000000..5792127d0 --- /dev/null +++ b/src/com/android/launcher3/anim/SpringAnimationHandler.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2017 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.anim; + +import android.support.animation.DynamicAnimation; +import android.support.animation.SpringAnimation; +import android.support.animation.SpringForce; +import android.support.annotation.IntDef; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; + +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.AlphabeticalAppsList; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +/** + * Handler class that manages springs for a set of views that should all move based on the same + * {@link MotionEvent}s. + * + * Supports using physics for X or Y translations. + */ +public class SpringAnimationHandler { + + private static final String TAG = "SpringAnimationHandler"; + private static final boolean DEBUG = false; + + private static final float DEFAULT_MAX_VALUE = 100; + private static final float DEFAULT_MIN_VALUE = -DEFAULT_MAX_VALUE; + + private static final float SPRING_DAMPING_RATIO = 0.55f; + private static final float MIN_SPRING_STIFFNESS = 580f; + private static final float MAX_SPRING_STIFFNESS = 900f; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({Y_DIRECTION, X_DIRECTION}) + public @interface Direction {} + public static final int Y_DIRECTION = 0; + public static final int X_DIRECTION = 1; + private int mDirection; + + private VelocityTracker mVelocityTracker; + private float mCurrentVelocity = 0; + private boolean mShouldComputeVelocity = false; + + private ArrayList<SpringAnimation> mAnimations = new ArrayList<>(); + + public SpringAnimationHandler(@Direction int direction) { + mDirection = direction; + mVelocityTracker = VelocityTracker.obtain(); + } + + public SpringAnimation add(View view, int position, AlphabeticalAppsList apps, int appsPerRow, + SpringAnimation recycle) { + int numPredictedApps = Math.min(appsPerRow, apps.getPredictedApps().size()); + int appPosition = getAppPosition(position, numPredictedApps, appsPerRow); + + int col = appPosition % appsPerRow; + int row = appPosition / appsPerRow; + + int numTotalRows = apps.getNumAppRows() - 1; // zero offset + if (row > (numTotalRows / 2)) { + // Mirror the rows so that the top row acts the same as the bottom row. + row = Math.abs(numTotalRows - row); + } + + // We manipulate the stiffness, min, and max values based on the items distance to the first + // row and the items distance to the center column to create the ^-shaped motion effect. + float rowFactor = (1 + row) * 0.5f; + float colFactor = getColumnFactor(col, appsPerRow); + + float minValue = DEFAULT_MIN_VALUE * (rowFactor + colFactor); + float maxValue = DEFAULT_MAX_VALUE * (rowFactor + colFactor); + + float stiffness = Utilities.boundToRange(MAX_SPRING_STIFFNESS - (row * 50f), + MIN_SPRING_STIFFNESS, MAX_SPRING_STIFFNESS); + + SpringAnimation animation = (recycle != null ? recycle : createSpringAnimation(view)) + .setStartVelocity(mCurrentVelocity) + .setMinValue(minValue) + .setMaxValue(maxValue); + animation.getSpring().setStiffness(stiffness); + + mAnimations.add(animation); + return animation; + } + + public SpringAnimation remove(SpringAnimation animation) { + animation.skipToEnd(); + mAnimations.remove(animation); + return animation; + } + + public void addMovement(MotionEvent event) { + int action = event.getActionMasked(); + if (DEBUG) Log.d(TAG, "addMovement#action=" + action); + switch (action) { + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_DOWN: + reset(); + break; + } + + getVelocityTracker().addMovement(event); + mShouldComputeVelocity = true; + } + + public void animateToFinalPosition(float position) { + if (DEBUG) Log.d(TAG, "animateToFinalPosition#computeVelocity=" + mShouldComputeVelocity); + + if (mShouldComputeVelocity) { + computeVelocity(); + setStartVelocity(mCurrentVelocity); + } + + int size = mAnimations.size(); + for (int i = 0; i < size; ++i) { + mAnimations.get(i).animateToFinalPosition(position); + } + + reset(); + } + + public void skipToEnd() { + if (DEBUG) Log.d(TAG, "setStartVelocity#skipToEnd"); + if (DEBUG) Log.v(TAG, "setStartVelocity#skipToEnd", new Exception()); + + int size = mAnimations.size(); + for (int i = 0; i < size; ++i) { + mAnimations.get(i).skipToEnd(); + } + } + + public void reset() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + mCurrentVelocity = 0; + } + + private void setStartVelocity(float velocity) { + int size = mAnimations.size(); + for (int i = 0; i < size; ++i) { + mAnimations.get(i).setStartVelocity(velocity); + } + } + + private void computeVelocity() { + getVelocityTracker().computeCurrentVelocity(300); + + mCurrentVelocity = isVerticalDirection() + ? getVelocityTracker().getYVelocity() + : getVelocityTracker().getXVelocity(); + mShouldComputeVelocity = false; + + if (DEBUG) Log.d(TAG, "computeVelocity=" + mCurrentVelocity); + } + + private boolean isVerticalDirection() { + return mDirection == Y_DIRECTION; + } + + private SpringAnimation createSpringAnimation(View view) { + DynamicAnimation.ViewProperty property = isVerticalDirection() + ? DynamicAnimation.TRANSLATION_Y + : DynamicAnimation.TRANSLATION_X; + + return new SpringAnimation(view, property, 0) + .setStartValue(1f) + .setSpring(new SpringForce(0) + .setDampingRatio(SPRING_DAMPING_RATIO)); + } + + /** + * @return The app position is the position of the app in the Adapter if we ignored all other + * view types. + * + * ie. The first predicted app is at position 0, and the first app of all apps is + * at {@param appsPerRow}. + */ + private int getAppPosition(int position, int numPredictedApps, int appsPerRow) { + int appPosition = position; + int numDividerViews = 1 + (numPredictedApps == 0 ? 0 : 1); + + int allAppsStartAt = numDividerViews + numPredictedApps; + if (numDividerViews == 1 || position < allAppsStartAt) { + appPosition -= 1; + } else { + // We cannot assume that the predicted row will always be full. + int numPredictedAppsOffset = appsPerRow - numPredictedApps; + appPosition = position + numPredictedAppsOffset - numDividerViews; + } + + return appPosition; + } + + /** + * Increase the column factor as the distance increases between the column and the center + * column(s). + */ + private float getColumnFactor(int col, int numCols) { + float centerColumn = numCols / 2; + int distanceToCenter = (int) Math.abs(col - centerColumn); + + boolean evenNumberOfColumns = numCols % 2 == 0; + if (evenNumberOfColumns && col < centerColumn) { + distanceToCenter -= 1; + } + + float factor = 0; + while (distanceToCenter > 0) { + if (distanceToCenter == 1) { + factor += 0.2f; + } else { + factor += 0.1f; + } + --distanceToCenter; + } + + return factor; + } + + private VelocityTracker getVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + return mVelocityTracker; + } +} |