mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-03-02 17:06:49 +00:00
* Overscroll at the top of all apps will occur when the user scrolls up, hits the top, and continues to scroll up. * Fixed bug where All Apps jumps when the user enters overscroll from a scroll that doesn't start at the bottom. * Fix bug where AllAppsRecyclerView stays translated even after the user has finished dragging. Bug: 62628421 Change-Id: Ia1d230a7cc07a7cf8c1a7c5211a025034ae5f6df
594 lines
23 KiB
Java
594 lines
23 KiB
Java
/*
|
|
* Copyright (C) 2015 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.allapps;
|
|
|
|
import android.animation.ObjectAnimator;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.support.v7.widget.RecyclerView;
|
|
import android.util.AttributeSet;
|
|
import android.util.Property;
|
|
import android.util.SparseIntArray;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
|
|
import com.android.launcher3.BaseRecyclerView;
|
|
import com.android.launcher3.BubbleTextView;
|
|
import com.android.launcher3.DeviceProfile;
|
|
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;
|
|
|
|
import java.util.List;
|
|
|
|
/**
|
|
* A RecyclerView with custom fast scroll support for the all apps view.
|
|
*/
|
|
public class AllAppsRecyclerView extends BaseRecyclerView {
|
|
|
|
private AlphabeticalAppsList mApps;
|
|
private AllAppsFastScrollHelper mFastScrollHelper;
|
|
private int mNumAppsPerRow;
|
|
|
|
// The specific view heights that we use to calculate scroll
|
|
private SparseIntArray mViewHeights = new SparseIntArray();
|
|
private SparseIntArray mCachedScrollPositions = new SparseIntArray();
|
|
|
|
// The empty-search result background
|
|
private AllAppsBackgroundDrawable mEmptySearchBackground;
|
|
private int mEmptySearchBackgroundTopOffset;
|
|
|
|
private SpringAnimationHandler mSpringAnimationHandler;
|
|
private OverScrollHelper mOverScrollHelper;
|
|
private VerticalPullDetector mPullDetector;
|
|
|
|
private float mContentTranslationY = 0;
|
|
public static final Property<AllAppsRecyclerView, Float> CONTENT_TRANS_Y =
|
|
new Property<AllAppsRecyclerView, Float>(Float.class, "appsRecyclerViewContentTransY") {
|
|
@Override
|
|
public Float get(AllAppsRecyclerView allAppsRecyclerView) {
|
|
return allAppsRecyclerView.getContentTranslationY();
|
|
}
|
|
|
|
@Override
|
|
public void set(AllAppsRecyclerView allAppsRecyclerView, Float y) {
|
|
allAppsRecyclerView.setContentTranslationY(y);
|
|
}
|
|
};
|
|
|
|
public AllAppsRecyclerView(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public AllAppsRecyclerView(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
this(context, attrs, defStyleAttr, 0);
|
|
}
|
|
|
|
public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
|
|
int defStyleRes) {
|
|
super(context, attrs, defStyleAttr);
|
|
Resources res = getResources();
|
|
addOnItemTouchListener(this);
|
|
mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
|
|
R.dimen.all_apps_empty_search_bg_top_offset);
|
|
|
|
mOverScrollHelper = new OverScrollHelper();
|
|
mPullDetector = new VerticalPullDetector(getContext());
|
|
mPullDetector.setListener(mOverScrollHelper);
|
|
mPullDetector.setDetectableScrollConditions(VerticalPullDetector.DIRECTION_BOTH, true);
|
|
}
|
|
|
|
public void setSpringAnimationHandler(SpringAnimationHandler springAnimationHandler) {
|
|
mSpringAnimationHandler = springAnimationHandler;
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
|
|
mPullDetector.onTouchEvent(ev);
|
|
return super.onInterceptTouchEvent(rv, ev) || mOverScrollHelper.isInOverScroll();
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent e) {
|
|
mPullDetector.onTouchEvent(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.
|
|
*/
|
|
public void setApps(AlphabeticalAppsList apps) {
|
|
mApps = apps;
|
|
mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
|
|
}
|
|
|
|
public AlphabeticalAppsList getApps() {
|
|
return mApps;
|
|
}
|
|
|
|
/**
|
|
* Sets the number of apps per row in this recycler view.
|
|
*/
|
|
public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
|
|
mNumAppsPerRow = numAppsPerRow;
|
|
|
|
RecyclerView.RecycledViewPool pool = getRecycledViewPool();
|
|
int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
|
|
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
|
|
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER, 1);
|
|
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1);
|
|
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow);
|
|
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, mNumAppsPerRow);
|
|
pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, 1);
|
|
}
|
|
|
|
/**
|
|
* Ensures that we can present a stable scrollbar for views of varying types by pre-measuring
|
|
* all the different view types.
|
|
*/
|
|
public void preMeasureViews(AllAppsGridAdapter adapter) {
|
|
View icon = adapter.onCreateViewHolder(this, AllAppsGridAdapter.VIEW_TYPE_ICON).itemView;
|
|
final int iconHeight = icon.getLayoutParams().height;
|
|
mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, iconHeight);
|
|
mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, iconHeight);
|
|
|
|
final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
|
|
getResources().getDisplayMetrics().widthPixels, View.MeasureSpec.AT_MOST);
|
|
final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
|
|
getResources().getDisplayMetrics().heightPixels, View.MeasureSpec.AT_MOST);
|
|
|
|
putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
|
|
AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER,
|
|
AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER);
|
|
putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
|
|
AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET);
|
|
putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
|
|
AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH);
|
|
|
|
if (FeatureFlags.DISCOVERY_ENABLED) {
|
|
putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
|
|
AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER);
|
|
putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
|
|
AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM);
|
|
}
|
|
}
|
|
|
|
private void putSameHeightFor(AllAppsGridAdapter adapter, int w, int h, int... viewTypes) {
|
|
View view = adapter.onCreateViewHolder(this, viewTypes[0]).itemView;
|
|
view.measure(w, h);
|
|
for (int viewType : viewTypes) {
|
|
mViewHeights.put(viewType, view.getMeasuredHeight());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scrolls this recycler view to the top.
|
|
*/
|
|
public void scrollToTop() {
|
|
// Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
|
|
if (mScrollbar != null) {
|
|
mScrollbar.reattachThumbToScroll();
|
|
}
|
|
scrollToPosition(0);
|
|
}
|
|
|
|
@Override
|
|
public void onDraw(Canvas c) {
|
|
c.translate(0, mContentTranslationY);
|
|
|
|
// Draw the background
|
|
if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
|
|
mEmptySearchBackground.draw(c);
|
|
}
|
|
|
|
super.onDraw(c);
|
|
}
|
|
|
|
public float getContentTranslationY() {
|
|
return mContentTranslationY;
|
|
}
|
|
|
|
/**
|
|
* Use this method instead of calling {@link #setTranslationY(float)}} directly to avoid drawing
|
|
* on top of other Views.
|
|
*/
|
|
public void setContentTranslationY(float y) {
|
|
mContentTranslationY = y;
|
|
invalidate();
|
|
}
|
|
|
|
@Override
|
|
protected boolean verifyDrawable(Drawable who) {
|
|
return who == mEmptySearchBackground || super.verifyDrawable(who);
|
|
}
|
|
|
|
@Override
|
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
updateEmptySearchBackgroundBounds();
|
|
}
|
|
|
|
public int getContainerType(View v) {
|
|
if (mApps.hasFilter()) {
|
|
return ContainerType.SEARCHRESULT;
|
|
} else {
|
|
if (v instanceof BubbleTextView) {
|
|
BubbleTextView icon = (BubbleTextView) v;
|
|
int position = getChildPosition(icon);
|
|
if (position != NO_POSITION) {
|
|
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
|
|
AlphabeticalAppsList.AdapterItem item = items.get(position);
|
|
if (item.viewType == AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON) {
|
|
return ContainerType.PREDICTION;
|
|
}
|
|
}
|
|
}
|
|
return ContainerType.ALLAPPS;
|
|
}
|
|
}
|
|
|
|
public void onSearchResultsChanged() {
|
|
// Always scroll the view to the top so the user can see the changed results
|
|
scrollToTop();
|
|
|
|
if (mApps.shouldShowEmptySearch()) {
|
|
if (mEmptySearchBackground == null) {
|
|
mEmptySearchBackground = DrawableFactory.get(getContext())
|
|
.getAllAppsBackground(getContext());
|
|
mEmptySearchBackground.setAlpha(0);
|
|
mEmptySearchBackground.setCallback(this);
|
|
updateEmptySearchBackgroundBounds();
|
|
}
|
|
mEmptySearchBackground.animateBgAlpha(1f, 150);
|
|
} else if (mEmptySearchBackground != null) {
|
|
// For the time being, we just immediately hide the background to ensure that it does
|
|
// not overlap with the results
|
|
mEmptySearchBackground.setBgAlpha(0f);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onInterceptTouchEvent(MotionEvent e) {
|
|
boolean result = super.onInterceptTouchEvent(e);
|
|
if (!result && e.getAction() == MotionEvent.ACTION_DOWN
|
|
&& mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
|
|
mEmptySearchBackground.setHotspot(e.getX(), e.getY());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Maps the touch (from 0..1) to the adapter position that should be visible.
|
|
*/
|
|
@Override
|
|
public String scrollToPositionAtProgress(float touchFraction) {
|
|
int rowCount = mApps.getNumAppRows();
|
|
if (rowCount == 0) {
|
|
return "";
|
|
}
|
|
|
|
// Stop the scroller if it is scrolling
|
|
stopScroll();
|
|
|
|
// Find the fastscroll section that maps to this touch fraction
|
|
List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
|
|
mApps.getFastScrollerSections();
|
|
AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
|
|
for (int i = 1; i < fastScrollSections.size(); i++) {
|
|
AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
|
|
if (info.touchFraction > touchFraction) {
|
|
break;
|
|
}
|
|
lastInfo = info;
|
|
}
|
|
|
|
// Update the fast scroll
|
|
int scrollY = getCurrentScrollY();
|
|
int availableScrollHeight = getAvailableScrollHeight();
|
|
mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
|
|
return lastInfo.sectionName;
|
|
}
|
|
|
|
@Override
|
|
public void onFastScrollCompleted() {
|
|
super.onFastScrollCompleted();
|
|
mFastScrollHelper.onFastScrollCompleted();
|
|
}
|
|
|
|
@Override
|
|
public void setAdapter(Adapter adapter) {
|
|
super.setAdapter(adapter);
|
|
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
|
public void onChanged() {
|
|
mCachedScrollPositions.clear();
|
|
}
|
|
});
|
|
mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter);
|
|
}
|
|
|
|
/**
|
|
* Updates the bounds for the scrollbar.
|
|
*/
|
|
@Override
|
|
public void onUpdateScrollbar(int dy) {
|
|
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
|
|
|
|
// Skip early if there are no items or we haven't been measured
|
|
if (items.isEmpty() || mNumAppsPerRow == 0) {
|
|
mScrollbar.setThumbOffsetY(-1);
|
|
return;
|
|
}
|
|
|
|
// Skip early if, there no child laid out in the container.
|
|
int scrollY = getCurrentScrollY();
|
|
if (scrollY < 0) {
|
|
mScrollbar.setThumbOffsetY(-1);
|
|
return;
|
|
}
|
|
|
|
// Only show the scrollbar if there is height to be scrolled
|
|
int availableScrollBarHeight = getAvailableScrollBarHeight();
|
|
int availableScrollHeight = getAvailableScrollHeight();
|
|
if (availableScrollHeight <= 0) {
|
|
mScrollbar.setThumbOffsetY(-1);
|
|
return;
|
|
}
|
|
|
|
if (mScrollbar.isThumbDetached()) {
|
|
if (!mScrollbar.isDraggingThumb()) {
|
|
// Calculate the current scroll position, the scrollY of the recycler view accounts
|
|
// for the view padding, while the scrollBarY is drawn right up to the background
|
|
// padding (ignoring padding)
|
|
int scrollBarY = (int)
|
|
(((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
|
|
|
|
int thumbScrollY = mScrollbar.getThumbOffsetY();
|
|
int diffScrollY = scrollBarY - thumbScrollY;
|
|
if (diffScrollY * dy > 0f) {
|
|
// User is scrolling in the same direction the thumb needs to catch up to the
|
|
// current scroll position. We do this by mapping the difference in movement
|
|
// from the original scroll bar position to the difference in movement necessary
|
|
// in the detached thumb position to ensure that both speed towards the same
|
|
// position at either end of the list.
|
|
if (dy < 0) {
|
|
int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY);
|
|
thumbScrollY += Math.max(offset, diffScrollY);
|
|
} else {
|
|
int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) /
|
|
(float) (availableScrollBarHeight - scrollBarY));
|
|
thumbScrollY += Math.min(offset, diffScrollY);
|
|
}
|
|
thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY));
|
|
mScrollbar.setThumbOffsetY(thumbScrollY);
|
|
if (scrollBarY == thumbScrollY) {
|
|
mScrollbar.reattachThumbToScroll();
|
|
}
|
|
} else {
|
|
// User is scrolling in an opposite direction to the direction that the thumb
|
|
// needs to catch up to the scroll position. Do nothing except for updating
|
|
// the scroll bar x to match the thumb width.
|
|
mScrollbar.setThumbOffsetY(thumbScrollY);
|
|
}
|
|
}
|
|
} else {
|
|
synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean supportsFastScrolling() {
|
|
// Only allow fast scrolling when the user is not searching, since the results are not
|
|
// grouped in a meaningful order
|
|
return !mApps.hasFilter();
|
|
}
|
|
|
|
@Override
|
|
public int getCurrentScrollY() {
|
|
// Return early if there are no items or we haven't been measured
|
|
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
|
|
if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) {
|
|
return -1;
|
|
}
|
|
|
|
// Calculate the y and offset for the item
|
|
View child = getChildAt(0);
|
|
int position = getChildPosition(child);
|
|
if (position == NO_POSITION) {
|
|
return -1;
|
|
}
|
|
return getPaddingTop() +
|
|
getCurrentScrollY(position, getLayoutManager().getDecoratedTop(child));
|
|
}
|
|
|
|
public int getCurrentScrollY(int position, int offset) {
|
|
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
|
|
AlphabeticalAppsList.AdapterItem posItem = position < items.size() ?
|
|
items.get(position) : null;
|
|
int y = mCachedScrollPositions.get(position, -1);
|
|
if (y < 0) {
|
|
y = 0;
|
|
for (int i = 0; i < position; i++) {
|
|
AlphabeticalAppsList.AdapterItem item = items.get(i);
|
|
if (AllAppsGridAdapter.isIconViewType(item.viewType)) {
|
|
// Break once we reach the desired row
|
|
if (posItem != null && posItem.viewType == item.viewType &&
|
|
posItem.rowIndex == item.rowIndex) {
|
|
break;
|
|
}
|
|
// Otherwise, only account for the first icon in the row since they are the same
|
|
// size within a row
|
|
if (item.rowAppIndex == 0) {
|
|
y += mViewHeights.get(item.viewType, 0);
|
|
}
|
|
} else {
|
|
// Rest of the views span the full width
|
|
y += mViewHeights.get(item.viewType, 0);
|
|
}
|
|
}
|
|
mCachedScrollPositions.put(position, y);
|
|
}
|
|
return y - offset;
|
|
}
|
|
|
|
/**
|
|
* Returns the available scroll height:
|
|
* AvailableScrollHeight = Total height of the all items - last page height
|
|
*/
|
|
@Override
|
|
protected int getAvailableScrollHeight() {
|
|
return getPaddingTop() + getCurrentScrollY(mApps.getAdapterItems().size(), 0)
|
|
- getHeight() + getPaddingBottom();
|
|
}
|
|
|
|
/**
|
|
* Updates the bounds of the empty search background.
|
|
*/
|
|
private void updateEmptySearchBackgroundBounds() {
|
|
if (mEmptySearchBackground == null) {
|
|
return;
|
|
}
|
|
|
|
// Center the empty search background on this new view bounds
|
|
int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2;
|
|
int y = mEmptySearchBackgroundTopOffset;
|
|
mEmptySearchBackground.setBounds(x, y,
|
|
x + mEmptySearchBackground.getIntrinsicWidth(),
|
|
y + mEmptySearchBackground.getIntrinsicHeight());
|
|
}
|
|
|
|
private class OverScrollHelper implements VerticalPullDetector.Listener {
|
|
|
|
private static final float MAX_RELEASE_VELOCITY = 5000; // px / s
|
|
private static final float MAX_OVERSCROLL_PERCENTAGE = 0.07f;
|
|
|
|
private boolean mIsInOverScroll;
|
|
|
|
// We use this value to calculate the actual amount the user has overscrolled.
|
|
private float mFirstDisplacement = 0;
|
|
|
|
private boolean mAlreadyScrollingUp;
|
|
private int mFirstScrollYOnScrollUp;
|
|
|
|
@Override
|
|
public void onDragStart(boolean start) {
|
|
}
|
|
|
|
@Override
|
|
public boolean onDrag(float displacement, float velocity) {
|
|
boolean isScrollingUp = displacement > 0;
|
|
if (isScrollingUp) {
|
|
if (!mAlreadyScrollingUp) {
|
|
mFirstScrollYOnScrollUp = getCurrentScrollY();
|
|
mAlreadyScrollingUp = true;
|
|
}
|
|
} else {
|
|
mAlreadyScrollingUp = false;
|
|
}
|
|
|
|
// Only enter overscroll if the user is interacting with the RecyclerView directly
|
|
// and if one of the following criteria are met:
|
|
// - User scrolls down when they're already at the bottom.
|
|
// - User starts scrolling up, hits the top, and continues scrolling up.
|
|
mIsInOverScroll = !mScrollbar.isDraggingThumb() &&
|
|
((!canScrollVertically(1) && displacement < 0) ||
|
|
(!canScrollVertically(-1) && isScrollingUp && mFirstScrollYOnScrollUp != 0));
|
|
|
|
if (mIsInOverScroll) {
|
|
if (Float.compare(mFirstDisplacement, 0) == 0) {
|
|
// Because users can scroll before entering overscroll, we need to
|
|
// subtract the amount where the user was not in overscroll.
|
|
mFirstDisplacement = displacement;
|
|
}
|
|
float overscrollY = displacement - mFirstDisplacement;
|
|
setContentTranslationY(getDampedOverScroll(overscrollY));
|
|
}
|
|
|
|
return mIsInOverScroll;
|
|
}
|
|
|
|
@Override
|
|
public void onDragEnd(float velocity, boolean fling) {
|
|
float y = getContentTranslationY();
|
|
if (Float.compare(y, 0) != 0) {
|
|
if (FeatureFlags.LAUNCHER3_PHYSICS) {
|
|
// We calculate our own velocity to give the springs the desired effect.
|
|
velocity = y / getDampedOverScroll(getHeight()) * MAX_RELEASE_VELOCITY;
|
|
// We want to negate the velocity because we are moving to 0 from -1 due to the
|
|
// downward motion. (y-axis -1 is above 0).
|
|
mSpringAnimationHandler.animateToPositionWithVelocity(0, -1, -velocity);
|
|
}
|
|
|
|
ObjectAnimator.ofFloat(AllAppsRecyclerView.this,
|
|
AllAppsRecyclerView.CONTENT_TRANS_Y, 0)
|
|
.setDuration(100)
|
|
.start();
|
|
}
|
|
mIsInOverScroll = false;
|
|
mFirstDisplacement = 0;
|
|
mFirstScrollYOnScrollUp = 0;
|
|
mAlreadyScrollingUp = false;
|
|
}
|
|
|
|
public boolean isInOverScroll() {
|
|
return mIsInOverScroll;
|
|
}
|
|
|
|
private float getDampedOverScroll(float y) {
|
|
return dampedOverScroll(y, getHeight()) * MAX_OVERSCROLL_PERCENTAGE;
|
|
}
|
|
|
|
/**
|
|
* This curve determines how the effect of scrolling over the limits of the page diminishes
|
|
* as the user pulls further and further from the bounds
|
|
*
|
|
* @param f The percentage of how much the user has overscrolled.
|
|
* @return A transformed percentage based on the influence curve.
|
|
*/
|
|
private float overScrollInfluenceCurve(float f) {
|
|
f -= 1.0f;
|
|
return f * f * f + 1.0f;
|
|
}
|
|
|
|
/**
|
|
* @param amount The original amount overscrolled.
|
|
* @param max The maximum amount that the View can overscroll.
|
|
* @return The dampened overscroll amount.
|
|
*/
|
|
private float dampedOverScroll(float amount, float max) {
|
|
float f = amount / max;
|
|
if (Float.compare(f, 0) == 0) return 0;
|
|
f = f / (Math.abs(f)) * (overScrollInfluenceCurve(Math.abs(f)));
|
|
|
|
// Clamp this factor, f, to -1 < f < 1
|
|
if (Math.abs(f) >= 1) {
|
|
f /= Math.abs(f);
|
|
}
|
|
|
|
return Math.round(f * max);
|
|
}
|
|
}
|
|
}
|