Merge "Pull up all apps interaction First phase implementation: dragging and animation interaction is implemented namely in two classes. ScrollGestureDetector and AllAppsTransitionController." into ub-launcher3-calgary

This commit is contained in:
Hyunyoung Song
2016-06-06 21:20:45 +00:00
committed by Android (Google) Code Review
12 changed files with 854 additions and 81 deletions
@@ -24,6 +24,9 @@ import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.config.FeatureFlags;
/**
* A base container view, which supports resizing.
*/
@@ -48,7 +51,11 @@ public abstract class BaseContainerView extends FrameLayout {
super(context, attrs, defStyleAttr);
int width = ((Launcher) context).getDeviceProfile().availableWidthPx;
mHorizontalPadding = DeviceProfile.getContainerPadding(context, width);
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && (this instanceof AllAppsContainerView)) {
mHorizontalPadding = 0;
} else {
mHorizontalPadding = DeviceProfile.getContainerPadding(context, width);
}
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.BaseContainerView, defStyleAttr, 0);
@@ -9,6 +9,9 @@ import android.view.ViewDebug;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.config.FeatureFlags;
public class InsettableFrameLayout extends FrameLayout implements
ViewGroup.OnHierarchyChangeListener, Insettable {
@@ -31,6 +34,9 @@ public class InsettableFrameLayout extends FrameLayout implements
lp.rightMargin += (newInsets.right - oldInsets.right);
lp.bottomMargin += (newInsets.bottom - oldInsets.bottom);
}
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && child instanceof AllAppsContainerView) {
lp.setMargins(0, 0, 0, lp.bottomMargin);
}
child.setLayoutParams(lp);
}
+18 -13
View File
@@ -92,6 +92,7 @@ import android.widget.Toast;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.allapps.DefaultAppSearchController;
import com.android.launcher3.compat.AppWidgetManagerCompat;
import com.android.launcher3.compat.LauncherActivityInfoCompat;
@@ -256,6 +257,7 @@ public class Launcher extends Activity
// Main container view for the all apps screen.
@Thunk AllAppsContainerView mAppsView;
AllAppsTransitionController mAllAppsController;
// Main container view and the model for the widget tray screen.
@Thunk WidgetsContainerView mWidgetsView;
@@ -413,7 +415,8 @@ public class Launcher extends Activity
mIconCache = app.getIconCache();
mDragController = new DragController(this);
mStateTransitionAnimation = new LauncherStateTransitionAnimation(this);
mAllAppsController = new AllAppsTransitionController(this);
mStateTransitionAnimation = new LauncherStateTransitionAnimation(this, mAllAppsController);
mAppWidgetManager = AppWidgetManagerCompat.getInstance(this);
@@ -1339,8 +1342,6 @@ public class Launcher extends Activity
* Finds all the views we need and configure them properly.
*/
private void setupViews() {
final DragController dragController = mDragController;
mLauncherView = findViewById(R.id.launcher);
mFocusHandler = (FocusIndicatorView) findViewById(R.id.focus_indicator);
mDragLayer = (DragLayer) findViewById(R.id.drag_layer);
@@ -1353,7 +1354,8 @@ public class Launcher extends Activity
mWorkspaceBackgroundDrawable = getResources().getDrawable(R.drawable.workspace_bg);
// Setup the drag layer
mDragLayer.setup(this, dragController);
mDragLayer.setup(this, mDragController, mAllAppsController);
// Setup the hotseat
mHotseat = (Hotseat) findViewById(R.id.hotseat);
@@ -1367,9 +1369,9 @@ public class Launcher extends Activity
// Setup the workspace
mWorkspace.setHapticFeedbackEnabled(false);
mWorkspace.setOnLongClickListener(this);
mWorkspace.setup(dragController);
mWorkspace.setup(mDragController);
mWorkspace.bindAndInitFirstWorkspaceScreen(null /* recycled qsb */);
dragController.addDragListener(mWorkspace);
mDragController.addDragListener(mWorkspace);
// Get the search/delete/uninstall bar
mSearchDropTargetBar = (SearchDropTargetBar)
@@ -1388,15 +1390,15 @@ public class Launcher extends Activity
}
// Setup the drag controller (drop targets have to be added in reverse order in priority)
dragController.setDragScoller(mWorkspace);
dragController.setScrollView(mDragLayer);
dragController.setMoveTarget(mWorkspace);
dragController.addDropTarget(mWorkspace);
mDragController.setDragScoller(mWorkspace);
mDragController.setScrollView(mDragLayer);
mDragController.setMoveTarget(mWorkspace);
mDragController.addDropTarget(mWorkspace);
if (mSearchDropTargetBar != null) {
mSearchDropTargetBar.setup(this, dragController);
mSearchDropTargetBar.setup(this, mDragController);
}
if (mAppInfoDropTargetBar != null) {
mAppInfoDropTargetBar.setup(this, dragController);
mAppInfoDropTargetBar.setup(this, mDragController);
}
if (TestingUtils.MEMORY_DUMP_ENABLED) {
@@ -1940,6 +1942,7 @@ public class Launcher extends Activity
if (mWorkspace.getChildCount() > 0) {
outState.putInt(RUNTIME_STATE_CURRENT_SCREEN,
mWorkspace.getCurrentPageOffsetFromCustomContent());
}
super.onSaveInstanceState(outState);
@@ -3163,6 +3166,8 @@ public class Launcher extends Activity
mWorkspace.startReordering(v);
} else {
showOverviewMode(true);
mHotseat.requestDisallowInterceptTouchEvent(true);
}
} else {
final boolean isAllAppsButton = inHotseat && isAllAppsButtonRank(
@@ -3318,7 +3323,7 @@ public class Launcher extends Activity
/**
* Shows the apps view.
*/
void showAppsView(boolean animated, boolean resetListToTop, boolean updatePredictedApps,
public void showAppsView(boolean animated, boolean resetListToTop, boolean updatePredictedApps,
boolean focusSearchBar) {
if (resetListToTop) {
mAppsView.scrollToTop();
@@ -33,6 +33,8 @@ import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.UiThreadCircularReveal;
import com.android.launcher3.widget.WidgetsContainerView;
@@ -82,6 +84,17 @@ import java.util.HashMap;
*/
public class LauncherStateTransitionAnimation {
/**
* animation used for all apps and widget tray when
*{@link FeatureFlags#LAUNCHER3_ALL_APPS_PULL_UP} is {@code false}
*/
public static final int CIRCULAR_REVEAL = 0;
/**
* animation used for all apps and not widget tray when
*{@link FeatureFlags#LAUNCHER3_ALL_APPS_PULL_UP} is {@code true}
*/
public static final int PULLUP = 1;
private static final float FINAL_REVEAL_ALPHA_FOR_WIDGETS = 0.3f;
/**
@@ -113,9 +126,11 @@ public class LauncherStateTransitionAnimation {
@Thunk Launcher mLauncher;
@Thunk AnimatorSet mCurrentAnimation;
AllAppsTransitionController mAllAppsController;
public LauncherStateTransitionAnimation(Launcher l) {
public LauncherStateTransitionAnimation(Launcher l, AllAppsTransitionController allAppsController) {
mLauncher = l;
mAllAppsController = allAppsController;
}
/**
@@ -154,9 +169,13 @@ public class LauncherStateTransitionAnimation {
}
}
};
int animType = CIRCULAR_REVEAL;
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
animType = PULLUP;
}
// Only animate the search bar if animating from spring loaded mode back to all apps
mCurrentAnimation = startAnimationToOverlay(fromWorkspaceState,
Workspace.State.NORMAL_HIDDEN, buttonView, toView, animated, cb);
Workspace.State.NORMAL_HIDDEN, buttonView, toView, animated, animType, cb);
}
/**
@@ -167,7 +186,7 @@ public class LauncherStateTransitionAnimation {
final WidgetsContainerView toView = mLauncher.getWidgetsView();
final View buttonView = mLauncher.getWidgetsButton();
mCurrentAnimation = startAnimationToOverlay(fromWorkspaceState,
Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, animated,
Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, animated, CIRCULAR_REVEAL,
new PrivateTransitionCallbacks(FINAL_REVEAL_ALPHA_FOR_WIDGETS){
@Override
void onTransitionComplete() {
@@ -189,8 +208,12 @@ public class LauncherStateTransitionAnimation {
}
if (fromState == Launcher.State.APPS || fromState == Launcher.State.APPS_SPRING_LOADED) {
int animType = CIRCULAR_REVEAL;
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
animType = PULLUP;
}
startAnimationToWorkspaceFromAllApps(fromWorkspaceState, toWorkspaceState,
animated, onCompleteRunnable);
animated, animType, onCompleteRunnable);
} else if (fromState == Launcher.State.WIDGETS ||
fromState == Launcher.State.WIDGETS_SPRING_LOADED) {
startAnimationToWorkspaceFromWidgets(fromWorkspaceState, toWorkspaceState,
@@ -208,7 +231,7 @@ public class LauncherStateTransitionAnimation {
private AnimatorSet startAnimationToOverlay(
final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState,
final View buttonView, final BaseContainerView toView,
final boolean animated, final PrivateTransitionCallbacks pCb) {
final boolean animated, int animType, final PrivateTransitionCallbacks pCb) {
final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
final Resources res = mLauncher.getResources();
final boolean material = Utilities.ATLEAST_LOLLIPOP;
@@ -225,12 +248,35 @@ public class LauncherStateTransitionAnimation {
// Cancel the current animation
cancelAnimation();
playCommonTransitionAnimations(toWorkspaceState, fromView, toView,
animated, initialized, animation, revealDuration, layerViews);
if (!FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
playCommonTransitionAnimations(toWorkspaceState, fromView, toView,
animated, initialized, animation, revealDuration, layerViews);
}
final View contentView = toView.getContentView();
if (animated && initialized) {
if (!animated || !initialized) {
toView.setTranslationX(0.0f);
toView.setTranslationY(0.0f);
toView.setScaleX(1.0f);
toView.setScaleY(1.0f);
toView.setAlpha(1.0f);
toView.setVisibility(View.VISIBLE);
toView.bringToFront();
// Show the content view
contentView.setVisibility(View.VISIBLE);
dispatchOnLauncherTransitionPrepare(fromView, animated, false);
dispatchOnLauncherTransitionStart(fromView, animated, false);
dispatchOnLauncherTransitionEnd(fromView, animated, false);
dispatchOnLauncherTransitionPrepare(toView, animated, false);
dispatchOnLauncherTransitionStart(toView, animated, false);
dispatchOnLauncherTransitionEnd(toView, animated, false);
pCb.onTransitionComplete();
return null;
}
if (animType == CIRCULAR_REVEAL) {
// Setup the reveal view animation
final View revealView = toView.getRevealView();
@@ -366,27 +412,40 @@ public class LauncherStateTransitionAnimation {
toView.post(startAnimRunnable);
return animation;
} else {
toView.setTranslationX(0.0f);
toView.setTranslationY(0.0f);
toView.setScaleX(1.0f);
toView.setScaleY(1.0f);
toView.setVisibility(View.VISIBLE);
toView.bringToFront();
} else if (animType == PULLUP) {
animation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchOnLauncherTransitionEnd(fromView, animated, false);
dispatchOnLauncherTransitionEnd(toView, animated, false);
cleanupAnimation();
pCb.onTransitionComplete();
}
// Show the content view
contentView.setVisibility(View.VISIBLE);
});
mAllAppsController.animateToAllApps(animation);
dispatchOnLauncherTransitionPrepare(fromView, animated, false);
dispatchOnLauncherTransitionStart(fromView, animated, false);
dispatchOnLauncherTransitionEnd(fromView, animated, false);
dispatchOnLauncherTransitionPrepare(toView, animated, false);
dispatchOnLauncherTransitionStart(toView, animated, false);
dispatchOnLauncherTransitionEnd(toView, animated, false);
pCb.onTransitionComplete();
return null;
final AnimatorSet stateAnimation = animation;
final Runnable startAnimRunnable = new Runnable() {
public void run() {
// Check that mCurrentAnimation hasn't changed while
// we waited for a layout/draw pass
if (mCurrentAnimation != stateAnimation)
return;
dispatchOnLauncherTransitionStart(fromView, animated, false);
dispatchOnLauncherTransitionStart(toView, animated, false);
toView.requestFocus();
stateAnimation.start();
}
};
toView.post(startAnimRunnable);
return animation;
}
return null;
}
/**
@@ -439,7 +498,7 @@ public class LauncherStateTransitionAnimation {
* Starts an animation to the workspace from the apps view.
*/
private void startAnimationToWorkspaceFromAllApps(final Workspace.State fromWorkspaceState,
final Workspace.State toWorkspaceState, final boolean animated,
final Workspace.State toWorkspaceState, final boolean animated, int type,
final Runnable onCompleteRunnable) {
AllAppsContainerView appsView = mLauncher.getAppsView();
// No alpha anim from all apps
@@ -476,7 +535,7 @@ public class LauncherStateTransitionAnimation {
// Only animate the search bar if animating to spring loaded mode from all apps
mCurrentAnimation = startAnimationToWorkspaceFromOverlay(fromWorkspaceState, toWorkspaceState,
mLauncher.getAllAppsButton(), appsView,
animated, onCompleteRunnable, cb);
animated, type, onCompleteRunnable, cb);
}
/**
@@ -506,7 +565,7 @@ public class LauncherStateTransitionAnimation {
mCurrentAnimation = startAnimationToWorkspaceFromOverlay(
fromWorkspaceState, toWorkspaceState,
mLauncher.getWidgetsButton(), widgetsView,
animated, onCompleteRunnable, cb);
animated, CIRCULAR_REVEAL, onCompleteRunnable, cb);
}
/**
@@ -598,7 +657,7 @@ public class LauncherStateTransitionAnimation {
private AnimatorSet startAnimationToWorkspaceFromOverlay(
final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState,
final View buttonView, final BaseContainerView fromView,
final boolean animated, final Runnable onCompleteRunnable,
final boolean animated, int animType, final Runnable onCompleteRunnable,
final PrivateTransitionCallbacks pCb) {
final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
final Resources res = mLauncher.getResources();
@@ -619,10 +678,29 @@ public class LauncherStateTransitionAnimation {
boolean multiplePagesVisible = toWorkspaceState.hasMultipleVisiblePages;
playCommonTransitionAnimations(toWorkspaceState, fromView, toView,
animated, initialized, animation, revealDuration, layerViews);
if (!FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
playCommonTransitionAnimations(toWorkspaceState, fromView, toView,
animated, initialized, animation, revealDuration, layerViews);
}
if (!animated || !initialized) {
mAllAppsController.finishPullDown();
fromView.setVisibility(View.GONE);
dispatchOnLauncherTransitionPrepare(fromView, animated, multiplePagesVisible);
dispatchOnLauncherTransitionStart(fromView, animated, true);
dispatchOnLauncherTransitionEnd(fromView, animated, true);
dispatchOnLauncherTransitionPrepare(toView, animated, multiplePagesVisible);
dispatchOnLauncherTransitionStart(toView, animated, true);
dispatchOnLauncherTransitionEnd(toView, animated, true);
pCb.onTransitionComplete();
if (animated && initialized) {
// Run any queued runnables
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
return null;
}
if (animType == CIRCULAR_REVEAL) {
final View revealView = fromView.getRevealView();
final View contentView = fromView.getContentView();
@@ -791,23 +869,59 @@ public class LauncherStateTransitionAnimation {
fromView.post(startAnimRunnable);
return animation;
} else /* if (!(animated && initialized)) */ {
fromView.setVisibility(View.GONE);
dispatchOnLauncherTransitionPrepare(fromView, animated, multiplePagesVisible);
dispatchOnLauncherTransitionStart(fromView, animated, true);
dispatchOnLauncherTransitionEnd(fromView, animated, true);
dispatchOnLauncherTransitionPrepare(toView, animated, multiplePagesVisible);
dispatchOnLauncherTransitionStart(toView, animated, true);
dispatchOnLauncherTransitionEnd(toView, animated, true);
pCb.onTransitionComplete();
} else if (animType == PULLUP) {
animation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchOnLauncherTransitionEnd(fromView, animated, false);
dispatchOnLauncherTransitionEnd(toView, animated, false);
cleanupAnimation();
pCb.onTransitionComplete();
}
// Run any queued runnables
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
});
mAllAppsController.animateToWorkspace(animation);
return null;
// Dispatch the prepare transition signal
dispatchOnLauncherTransitionPrepare(fromView, animated, false);
dispatchOnLauncherTransitionPrepare(toView, animated, false);
animation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchOnLauncherTransitionEnd(fromView, animated, true);
dispatchOnLauncherTransitionEnd(toView, animated, true);
// Run any queued runnables
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
// This can hold unnecessary references to views.
cleanupAnimation();
pCb.onTransitionComplete();
}
});
final AnimatorSet stateAnimation = animation;
final Runnable startAnimRunnable = new Runnable() {
public void run() {
// Check that mCurrentAnimation hasn't changed while
// we waited for a layout/draw pass
if (mCurrentAnimation != stateAnimation)
return;
dispatchOnLauncherTransitionStart(fromView, animated, false);
dispatchOnLauncherTransitionStart(toView, animated, false);
// Focus the new view
toView.requestFocus();
stateAnimation.start();
}
};
fromView.post(startAnimRunnable);
return animation;
}
return null;
}
/**
@@ -21,6 +21,8 @@ import android.content.Context;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import com.android.launcher3.util.TouchController;
/**
* Detects pinches and animates the Workspace to/from overview mode.
*
@@ -30,7 +32,8 @@ import android.view.ScaleGestureDetector;
* @see PinchThresholdManager
* @see PinchAnimationManager
*/
public class PinchToOverviewListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
public class PinchToOverviewListener extends ScaleGestureDetector.SimpleOnScaleGestureListener
implements TouchController {
private static final float OVERVIEW_PROGRESS = 0f;
private static final float WORKSPACE_PROGRESS = 1f;
/**
@@ -63,15 +66,16 @@ public class PinchToOverviewListener extends ScaleGestureDetector.SimpleOnScaleG
return mPinchStarted;
}
public void onTouchEvent(MotionEvent ev) {
public boolean onTouchEvent(MotionEvent ev) {
if (mPinchStarted) {
if (ev.getPointerCount() > 2) {
// Using more than two fingers causes weird behavior, so just cancel the pinch.
cancelPinch(mPreviousProgress, -1);
} else {
mPinchDetector.onTouchEvent(ev);
return mPinchDetector.onTouchEvent(ev);
}
}
return false;
}
@Override
@@ -20,16 +20,20 @@ import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.text.method.TextKeyListener;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.LinearLayout;
import com.android.launcher3.AppInfo;
import com.android.launcher3.BaseContainerView;
@@ -40,6 +44,7 @@ import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DragSource;
import com.android.launcher3.DropTarget;
import com.android.launcher3.ExtendedEditText;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.ItemInfo;
import com.android.launcher3.Launcher;
@@ -178,9 +183,13 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
mApps.setAdapter(mAdapter);
mLayoutManager = mAdapter.getLayoutManager();
mItemDecoration = mAdapter.getItemDecoration();
mRecyclerViewTopBottomPadding =
res.getDimensionPixelSize(R.dimen.all_apps_list_top_bottom_padding);
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
mRecyclerViewTopBottomPadding = 0;
setPadding(0, 0, 0, 0);
} else {
mRecyclerViewTopBottomPadding =
res.getDimensionPixelSize(R.dimen.all_apps_list_top_bottom_padding);
}
mSearchQueryBuilder = new SpannableStringBuilder();
Selection.setSelection(mSearchQueryBuilder, 0);
}
@@ -220,6 +229,13 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
mApps.removeApps(apps);
}
public void setSearchBarVisible(boolean visible) {
if (visible) {
mSearchBarController.setVisibility(View.VISIBLE);
} else {
mSearchBarController.setVisibility(View.INVISIBLE);
}
}
/**
* Sets the search bar that shows above the a-z list.
*/
@@ -239,6 +255,10 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
mAppsRecyclerView.scrollToTop();
}
public boolean isScrollAtTop() {
return ((LinearLayoutManager) mAppsRecyclerView.getLayoutManager())
.findFirstVisibleItemPosition() == 1;
}
/**
* Focuses the search field and begins an app search.
*/
@@ -321,13 +341,33 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
MeasureSpec.getSize(widthMeasureSpec) - mHorizontalPadding,
MeasureSpec.getSize(heightMeasureSpec));
DeviceProfile grid = mLauncher.getDeviceProfile();
int availableWidth = (!mContentBounds.isEmpty() ? mContentBounds.width() :
MeasureSpec.getSize(widthMeasureSpec))
- 2 * mAppsRecyclerView.getMaxScrollbarWidth();
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
if (mNumAppsPerRow != grid.inv.numColumns ||
mNumPredictedAppsPerRow != grid.inv.numColumns) {
mNumAppsPerRow = grid.inv.numColumns;
mNumPredictedAppsPerRow = grid.inv.numColumns;
mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow);
mAdapter.setNumAppsPerRow(mNumAppsPerRow);
mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, new FullMergeAlgorithm());
if (mNumAppsPerRow > 0) {
int iconSize = availableWidth / mNumAppsPerRow;
int iconSpacing = (iconSize - grid.allAppsIconSizePx) / 2;
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
// --- remove START when {@code FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP} is enabled. ---
// Update the number of items in the grid before we measure the view
// TODO: mSectionNamesMargin is currently 0, but also account for it,
// if it's enabled in the future.
int availableWidth = (!mContentBounds.isEmpty() ? mContentBounds.width() :
MeasureSpec.getSize(widthMeasureSpec))
- 2 * mAppsRecyclerView.getMaxScrollbarWidth();
DeviceProfile grid = mLauncher.getDeviceProfile();
grid.updateAppsViewNumCols(getResources(), availableWidth);
if (mNumAppsPerRow != grid.allAppsNumCols ||
mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) {
@@ -346,6 +386,8 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
mAdapter.setNumAppsPerRow(mNumAppsPerRow);
mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm);
// TODO: should we not do all this complicated computation but just match the
// numAppsPerRow with the workspace?
if (mNumAppsPerRow > 0) {
int iconSize = availableWidth / mNumAppsPerRow;
int iconSpacing = (iconSize - grid.allAppsIconSizePx) / 2;
@@ -353,6 +395,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
}
}
// --- remove END when {@code FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP} is enabled. ---
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@@ -385,6 +428,24 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
MarginLayoutParams lp = (MarginLayoutParams) mSearchContainer.getLayoutParams();
lp.leftMargin = bgPadding.left;
lp.rightMargin = bgPadding.right;
if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
MarginLayoutParams mlp = (MarginLayoutParams) mAppsRecyclerView.getLayoutParams();
int navBarHeight = 84; /* replace with mInset.height() in dragLayer */
DeviceProfile grid = mLauncher.getDeviceProfile();
int height = navBarHeight + grid.hotseatCellHeightPx;
mlp.topMargin = height;
mAppsRecyclerView.setLayoutParams(mlp);
LinearLayout.LayoutParams llp =
(LinearLayout.LayoutParams) mSearchInput.getLayoutParams();
llp.topMargin = navBarHeight;
mSearchInput.setLayoutParams(llp);
lp.height = height;
}
mSearchContainer.setLayoutParams(lp);
}
@@ -437,6 +498,7 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc
// Return early if this is not initiated from a touch
if (!v.isInTouchMode()) return false;
// When we have exited all apps or are in transition, disregard long clicks
if (!mLauncher.isAppsViewVisible() ||
mLauncher.getWorkspace().isSwitchingState()) return false;
// Return if global dragging is not enabled
@@ -49,6 +49,9 @@ public abstract class AllAppsSearchBarController
protected DefaultAppSearchAlgorithm mSearchAlgorithm;
protected InputMethodManager mInputMethodManager;
public void setVisibility(int visibility) {
mInput.setVisibility(visibility);
}
/**
* Sets the references to the apps model and the search result callback.
*/
@@ -0,0 +1,347 @@
package com.android.launcher3.allapps;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import com.android.launcher3.Hotseat;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
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 static final float ANIMATION_DURATION = 500;
private AllAppsContainerView mAppsView;
private Hotseat mHotseat;
private Drawable mHotseatBackground;
private float mHotseatAlpha;
private View mWorkspaceCurPage;
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 float mAnimationDuration;
private float mCurY;
private AnimatorSet mCurrentAnimation;
public AllAppsTransitionController(Launcher launcher) {
mLauncher = launcher;
mDetector = new VerticalPullDetector(launcher);
mDetector.setListener(this);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
init();
if (mLauncher.getWorkspace().isInOverviewMode() ||
mLauncher.isWidgetsViewVisible()) {
return false;
}
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mDetector.setAllAppsState(mLauncher.isAllAppsVisible(), mAppsView.isScrollAtTop());
}
mDetector.onTouchEvent(ev);
return mDetector.mScrolling;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return mDetector.onTouchEvent(ev);
}
private void init() {
if (mAppsView != null) {
return;
}
mAppsView = mLauncher.getAppsView();
mHotseat = mLauncher.getHotseat();
mWorkspaceCurPage = mLauncher.getWorkspace().getChildAt(
mLauncher.getWorkspace().getCurrentPage());
if (mHotseatBackground == null) {
mHotseatBackground = mHotseat.getBackground();
mHotseatAlpha = mHotseatBackground.getAlpha() / 255f;
}
}
@Override
public void onScrollStart(boolean start) {
cancelAnimation();
mCurrentAnimation = LauncherAnimUtils.createAnimatorSet();
mCurY = mAppsView.getTranslationY();
preparePull(start);
}
/**
* @param start {@code true} if start of new drag.
*/
public void preparePull(boolean start) {
if (start) {
if (!mLauncher.isAllAppsVisible()) {
mHotseat.setBackground(null);
mAppsView.setVisibility(View.VISIBLE);
mAppsView.getContentView().setVisibility(View.VISIBLE);
mAppsView.setAlpha(mHotseatAlpha);
mAppsView.setSearchBarVisible(false);
if (mTranslation < 0) {
mTranslation = mHotseat.getTop();
setProgress(mTranslation);
}
} else {
mLauncher.getWorkspace().setVisibility(View.VISIBLE);
mLauncher.getWorkspace().setAlpha(1f);
mLauncher.getWorkspace().onLauncherTransitionPrepare(mLauncher, false, false);
mWorkspaceCurPage.setVisibility(View.VISIBLE);
mAppsView.setSearchBarVisible(false);
setLightStatusBar(false);
}
}
mHotseat.setVisibility(View.VISIBLE);
mHotseat.bringToFront();
}
private void setLightStatusBar(boolean enable) {
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);
}
}
private final Interpolator mAlphaInterpolator = new DecelerateInterpolator(.5f);
@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) {
mProgressTransY = progress;
float alpha = calcAlphaAllApps(progress);
float workspaceHotseatAlpha = Math.max(mHotseatAlpha, 1 - alpha);
setTransAndAlpha(mAppsView, progress, Math.max(mHotseatAlpha, alpha));
setTransAndAlpha(mWorkspaceCurPage, -mTranslation + progress, workspaceHotseatAlpha);
setTransAndAlpha(mHotseat, -mTranslation + progress, workspaceHotseatAlpha);
}
public float getProgress() {
return mProgressTransY;
}
private float calcAlphaAllApps(float progress) {
return mAlphaInterpolator.getInterpolation((mTranslation - progress)/mTranslation);
}
private void setTransAndAlpha(View v, float transY, float alpha) {
if (v != null) {
v.setTranslationY(transY);
v.setAlpha(alpha);
}
}
@Override
public void onScrollEnd(float velocity, boolean fling) {
if (mAppsView == null) {
return; // early termination.
}
if (fling) {
if (velocity < 0) {
calculateDuration(velocity, mAppsView.getTranslationY());
showAppsView(); // Flinging in UP direction
} else {
calculateDuration(velocity, Math.abs(mTranslation - mAppsView.getTranslationY()));
showWorkspace(); // Flinging in DOWN direction
}
// snap to top or bottom using the release velocity
} else {
if (mAppsView.getTranslationY() > mTranslation / 2) {
calculateDuration(velocity, Math.abs(mTranslation - mAppsView.getTranslationY()));
showWorkspace(); // Released in the bottom half
} else {
calculateDuration(velocity, Math.abs(mAppsView.getTranslationY()));
showAppsView(); // Released in the top half
}
}
}
private void calculateDuration(float velocity, float disp) {
// TODO: make these values constants after tuning.
float velocityDivisor = Math.max(1, 0.75f * velocity);
float travelDistance = Math.max(0.2f, disp / mTranslation);
mAnimationDuration = Math.max(100, ANIMATION_DURATION / velocityDivisor * travelDistance);
if (true) { // MERONG
Log.d(TAG, String.format("calculateDuration=%f, v=%f, d=%f", mAnimationDuration, velocity, disp));
}
}
/**
* Depending on the current state of the launcher, either just
* 1) animate
* 2) animate and do all the state updates.
*/
private void showAppsView() {
if (mLauncher.isAllAppsVisible()) {
animateToAllApps(mCurrentAnimation);
mCurrentAnimation.start();
} else {
mLauncher.showAppsView(true /* animated */, true /* resetListToTop */,
true /* updatePredictedApps */, false /* focusSearchBar */);
}
}
/**
* Depending on the current state of the launcher, either just
* 1) animate
* 2) animate and do all the state updates.
*/
private void showWorkspace() {
if (mLauncher.isAllAppsVisible()) {
mLauncher.showWorkspace(true /* animated */);
} else {
animateToWorkspace(mCurrentAnimation);
mCurrentAnimation.start();
}
}
public void animateToAllApps(AnimatorSet animationOut) {
if ((mAppsView = mLauncher.getAppsView()) == null || animationOut == null){
return;
}
if (!mDetector.mScrolling) {
preparePull(true);
}
mCurY = mAppsView.getTranslationY();
final float fromAllAppsTop = mAppsView.getTranslationY();
final float toAllAppsTop = 0;
ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
fromAllAppsTop, toAllAppsTop);
driftAndAlpha.setDuration((long) mAnimationDuration);
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;
}
private void finishPullUp() {
mAppsView.setSearchBarVisible(true);
mHotseat.setVisibility(View.INVISIBLE);
setProgress(0f);
setLightStatusBar(true);
}
public void animateToWorkspace(AnimatorSet animationOut) {
if ((mAppsView = mLauncher.getAppsView()) == null || animationOut == null){
return;
}
if(!mDetector.mScrolling) {
preparePull(true);
}
final float fromAllAppsTop = mAppsView.getTranslationY();
final float toAllAppsTop = mTranslation;
ObjectAnimator driftAndAlpha = ObjectAnimator.ofFloat(this, "progress",
fromAllAppsTop, toAllAppsTop);
driftAndAlpha.setDuration((long) mAnimationDuration);
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;
}
public void finishPullDown() {
mAppsView.setVisibility(View.INVISIBLE);
mHotseat.setBackground(mHotseatBackground);
mHotseat.setVisibility(View.VISIBLE);
setProgress(mTranslation);
setLightStatusBar(false);
}
private void cancelAnimation() {
if (mCurrentAnimation != null) {
mCurrentAnimation.cancel();
mCurrentAnimation = null;
}
}
private void cleanUpAnimation() {
mCurrentAnimation = null;
}
}
@@ -0,0 +1,196 @@
package com.android.launcher3.allapps;
import android.content.Context;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
/**
* One dimensional scroll gesture detector for all apps container pull up interaction.
*/
public class VerticalPullDetector {
private static final String TAG = "ScrollGesture";
private static final boolean DBG = false;
private float mTouchSlop;
private boolean mAllAppsVisible;
private boolean mAllAppsScrollAtTop;
/**
* The minimum release velocity in pixels per millisecond that triggers fling..
*/
private static final float RELEASE_VELOCITY_PX_MS = 1.7f;
/**
* The time constant used to calculate dampening in the low-pass filter of scroll velocity.
* Cutoff frequency is set at 10 Hz.
*/
public static final float SCROLL_VELOCITY_DAMPENING_RC = 1000f / (2f * (float) Math.PI * 10);
/* Scroll state, this is set to true during dragging and animation. */
boolean mScrolling;
float mDownY;
float mDownMillis;
float mLastY;
float mLastMillis;
float mVelocity;
float mLastDisplacement;
float mDisplacement;
/* scroll started during previous animation */
boolean mSubtractSlop = true;
/* Client of this gesture detector can register a callback. */
Listener mListener;
public void setListener(Listener l) {
mListener = l;
}
interface Listener{
/**
* @param start when should
*/
void onScrollStart(boolean start);
boolean onScroll(float displacement, float velocity);
void onScrollEnd(float velocity, boolean fling);
}
public VerticalPullDetector(Context context) {
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
public void setAllAppsState(boolean allAppsVisible, boolean scrollAtTop) {
mAllAppsVisible = allAppsVisible;
mAllAppsScrollAtTop = scrollAtTop;
}
private boolean shouldScrollStart() {
if (mAllAppsVisible && mDisplacement > mTouchSlop && mAllAppsScrollAtTop) {
return true;
}
if (!mAllAppsVisible && mDisplacement < -mTouchSlop) {
return true;
}
return false;
}
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownMillis = ev.getDownTime();
mDownY = ev.getY();
mLastDisplacement = 0;
mVelocity = 0;
if (mScrolling) {
reportScrollStart(true /* recatch */);
}
break;
case MotionEvent.ACTION_MOVE:
mDisplacement = computeDisplacement(ev);
mVelocity = computeVelocity(ev, mVelocity);
if (!mScrolling && shouldScrollStart()) {
mScrolling = true;
reportScrollStart(false /* recatch */);
}
if (mScrolling && mListener != null) {
reportScroll();
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// These are synthetic events and there is no need to update internal values.
if (mScrolling && mListener != null) {
reportScrollEnd();
}
break;
default:
//TODO: add multi finger tracking by tracking active pointer.
break;
}
// Do house keeping.
mLastDisplacement = mDisplacement;
mLastY = ev.getY();
mLastMillis = ev.getEventTime();
return true;
}
public void finishedScrolling() {
mScrolling = false;
}
private boolean reportScrollStart(boolean recatch) {
mListener.onScrollStart(!recatch);
if (DBG) {
Log.d(TAG, "onScrollStart recatch:" + recatch);
}
return true;
}
private boolean reportScroll() {
float delta = mDisplacement - mLastDisplacement;
if (delta != 0) {
if (DBG) {
Log.d(TAG, String.format("onScroll disp=%.1f, velocity=%.1f",
mDisplacement, mVelocity));
}
return mListener.onScroll(mDisplacement - (mSubtractSlop? mTouchSlop : 0), mVelocity);
}
return true;
}
private void reportScrollEnd() {
if (DBG) {
Log.d(TAG, String.format("onScrolEnd disp=%.1f, velocity=%.1f",
mDisplacement, mVelocity));
}
mListener.onScrollEnd(mVelocity, Math.abs(mVelocity) > RELEASE_VELOCITY_PX_MS);
}
/**
* Computes the damped velocity using the two motion events and the previous velocity.
*/
private float computeVelocity(MotionEvent to, float previousVelocity) {
float delta = computeDelta(to);
float deltaTimeMillis = to.getEventTime() - mLastMillis;
float velocity = (deltaTimeMillis > 0) ? (delta / deltaTimeMillis) : 0;
if (Math.abs(previousVelocity) < 0.001f) {
return velocity;
}
float alpha = computeDampeningFactor(deltaTimeMillis);
return interpolate(previousVelocity, velocity, alpha);
}
private float computeDisplacement(MotionEvent to) {
return to.getY() - mDownY;
}
private float computeDelta(MotionEvent to) {
return to.getY() - mLastY;
}
/**
* Returns a time-dependent dampening factor using delta time.
*/
private static float computeDampeningFactor(float deltaTime) {
return deltaTime / (SCROLL_VELOCITY_DAMPENING_RC + deltaTime);
}
/**
* Returns the linear interpolation between two values
*/
private static float interpolate(float from, float to, float alpha) {
return (1.0f - alpha) * from + alpha * to;
}
}
@@ -46,6 +46,7 @@ import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.accessibility.DragViewStateAnnouncer;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.TouchController;
import java.util.ArrayList;
import java.util.HashSet;
@@ -53,7 +54,7 @@ import java.util.HashSet;
/**
* Class for initiating a drag within a view or across multiple views.
*/
public class DragController implements DragDriver.EventListener {
public class DragController implements DragDriver.EventListener, TouchController {
private static final String TAG = "Launcher.DragController";
/** Indicates the drag is a move. */
@@ -43,6 +43,7 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.launcher3.AppWidgetResizeFrame;
import com.android.launcher3.BaseContainerView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.ItemInfo;
@@ -56,10 +57,13 @@ import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.TouchController;
import java.util.ArrayList;
@@ -117,6 +121,11 @@ public class DragLayer extends InsettableFrameLayout {
// Related to pinch-to-go-to-overview gesture.
private PinchToOverviewListener mPinchListener = null;
// Handles all apps pull up interaction
private AllAppsTransitionController mAllAppsController;
private TouchController mActiveController;
/**
* Used to create a new DragLayer from XML.
*
@@ -138,9 +147,11 @@ public class DragLayer extends InsettableFrameLayout {
mIsRtl = Utilities.isRtl(res);
}
public void setup(Launcher launcher, DragController controller) {
public void setup(Launcher launcher, DragController dragController,
AllAppsTransitionController allAppsTransitionController) {
mLauncher = launcher;
mDragController = controller;
mDragController = dragController;
mAllAppsController = allAppsTransitionController;
boolean isAccessibilityEnabled = ((AccessibilityManager) mLauncher.getSystemService(
Context.ACCESSIBILITY_SERVICE)).isEnabled();
@@ -246,6 +257,7 @@ public class DragLayer extends InsettableFrameLayout {
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN) {
if (handleTouchDown(ev, true)) {
return true;
@@ -258,11 +270,21 @@ public class DragLayer extends InsettableFrameLayout {
}
clearAllResizeFrames();
if (mPinchListener != null && mPinchListener.onInterceptTouchEvent(ev)) {
// Stop listening for scrolling etc. (onTouchEvent() handles the rest of the pinch.)
if (mDragController.onInterceptTouchEvent(ev)) {
mActiveController = mDragController;
return true;
}
return mDragController.onInterceptTouchEvent(ev);
if (mAllAppsController.onInterceptTouchEvent(ev)) {
mActiveController = mAllAppsController;
return true;
}
if (mPinchListener != null && mPinchListener.onInterceptTouchEvent(ev)) {
// Stop listening for scrolling etc. (onTouchEvent() handles the rest of the pinch.)
mActiveController = mPinchListener;
return true;
}
return false;
}
@Override
@@ -375,11 +397,6 @@ public class DragLayer extends InsettableFrameLayout {
int x = (int) ev.getX();
int y = (int) ev.getY();
// This is only reached if a pinch was started from onInterceptTouchEvent();
// this continues sending events for it.
if (mPinchListener != null) {
mPinchListener.onTouchEvent(ev);
}
if (action == MotionEvent.ACTION_DOWN) {
if (handleTouchDown(ev, false)) {
@@ -406,7 +423,10 @@ public class DragLayer extends InsettableFrameLayout {
}
}
if (handled) return true;
return mDragController.onTouchEvent(ev);
if (mActiveController != null) {
return mActiveController.onTouchEvent(ev);
}
return false;
}
@Override
@@ -0,0 +1,8 @@
package com.android.launcher3.util;
import android.view.MotionEvent;
public interface TouchController {
boolean onTouchEvent(MotionEvent ev);
boolean onInterceptTouchEvent(MotionEvent ev);
}