Home | History | Annotate | Download | only in allapps
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.launcher3.allapps;
     17 
     18 import android.animation.ObjectAnimator;
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.graphics.Canvas;
     22 import android.graphics.drawable.Drawable;
     23 import android.support.v7.widget.RecyclerView;
     24 import android.util.AttributeSet;
     25 import android.util.Property;
     26 import android.util.SparseIntArray;
     27 import android.view.MotionEvent;
     28 import android.view.View;
     29 
     30 import com.android.launcher3.BaseRecyclerView;
     31 import com.android.launcher3.BubbleTextView;
     32 import com.android.launcher3.DeviceProfile;
     33 import com.android.launcher3.ItemInfo;
     34 import com.android.launcher3.R;
     35 import com.android.launcher3.anim.SpringAnimationHandler;
     36 import com.android.launcher3.config.FeatureFlags;
     37 import com.android.launcher3.graphics.DrawableFactory;
     38 import com.android.launcher3.logging.UserEventDispatcher.LogContainerProvider;
     39 import com.android.launcher3.touch.OverScroll;
     40 import com.android.launcher3.touch.SwipeDetector;
     41 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
     42 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
     43 
     44 import java.util.List;
     45 
     46 /**
     47  * A RecyclerView with custom fast scroll support for the all apps view.
     48  */
     49 public class AllAppsRecyclerView extends BaseRecyclerView implements LogContainerProvider {
     50 
     51     private AlphabeticalAppsList mApps;
     52     private AllAppsFastScrollHelper mFastScrollHelper;
     53     private int mNumAppsPerRow;
     54 
     55     // The specific view heights that we use to calculate scroll
     56     private SparseIntArray mViewHeights = new SparseIntArray();
     57     private SparseIntArray mCachedScrollPositions = new SparseIntArray();
     58 
     59     // The empty-search result background
     60     private AllAppsBackgroundDrawable mEmptySearchBackground;
     61     private int mEmptySearchBackgroundTopOffset;
     62 
     63     private SpringAnimationHandler mSpringAnimationHandler;
     64     private OverScrollHelper mOverScrollHelper;
     65     private SwipeDetector mPullDetector;
     66 
     67     private float mContentTranslationY = 0;
     68     public static final Property<AllAppsRecyclerView, Float> CONTENT_TRANS_Y =
     69             new Property<AllAppsRecyclerView, Float>(Float.class, "appsRecyclerViewContentTransY") {
     70                 @Override
     71                 public Float get(AllAppsRecyclerView allAppsRecyclerView) {
     72                     return allAppsRecyclerView.getContentTranslationY();
     73                 }
     74 
     75                 @Override
     76                 public void set(AllAppsRecyclerView allAppsRecyclerView, Float y) {
     77                     allAppsRecyclerView.setContentTranslationY(y);
     78                 }
     79             };
     80 
     81     public AllAppsRecyclerView(Context context) {
     82         this(context, null);
     83     }
     84 
     85     public AllAppsRecyclerView(Context context, AttributeSet attrs) {
     86         this(context, attrs, 0);
     87     }
     88 
     89     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
     90         this(context, attrs, defStyleAttr, 0);
     91     }
     92 
     93     public AllAppsRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
     94             int defStyleRes) {
     95         super(context, attrs, defStyleAttr);
     96         Resources res = getResources();
     97         addOnItemTouchListener(this);
     98         mEmptySearchBackgroundTopOffset = res.getDimensionPixelSize(
     99                 R.dimen.all_apps_empty_search_bg_top_offset);
    100 
    101         mOverScrollHelper = new OverScrollHelper();
    102         mPullDetector = new SwipeDetector(getContext(), mOverScrollHelper, SwipeDetector.VERTICAL);
    103         mPullDetector.setDetectableScrollConditions(SwipeDetector.DIRECTION_BOTH, true);
    104     }
    105 
    106     public void setSpringAnimationHandler(SpringAnimationHandler springAnimationHandler) {
    107         if (FeatureFlags.LAUNCHER3_PHYSICS) {
    108             mSpringAnimationHandler = springAnimationHandler;
    109             addOnScrollListener(new SpringMotionOnScrollListener());
    110         }
    111     }
    112 
    113     @Override
    114     public boolean onTouchEvent(MotionEvent e) {
    115         mPullDetector.onTouchEvent(e);
    116         if (FeatureFlags.LAUNCHER3_PHYSICS && mSpringAnimationHandler != null) {
    117             mSpringAnimationHandler.addMovement(e);
    118         }
    119         return super.onTouchEvent(e);
    120     }
    121 
    122     /**
    123      * Sets the list of apps in this view, used to determine the fastscroll position.
    124      */
    125     public void setApps(AlphabeticalAppsList apps) {
    126         mApps = apps;
    127         mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
    128     }
    129 
    130     public AlphabeticalAppsList getApps() {
    131         return mApps;
    132     }
    133 
    134     /**
    135      * Sets the number of apps per row in this recycler view.
    136      */
    137     public void setNumAppsPerRow(DeviceProfile grid, int numAppsPerRow) {
    138         mNumAppsPerRow = numAppsPerRow;
    139 
    140         RecyclerView.RecycledViewPool pool = getRecycledViewPool();
    141         int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
    142         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH, 1);
    143         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER, 1);
    144         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET, 1);
    145         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_ICON, approxRows * mNumAppsPerRow);
    146         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, mNumAppsPerRow);
    147         pool.setMaxRecycledViews(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER, 1);
    148     }
    149 
    150     /**
    151      * Ensures that we can present a stable scrollbar for views of varying types by pre-measuring
    152      * all the different view types.
    153      */
    154     public void preMeasureViews(AllAppsGridAdapter adapter) {
    155         View icon = adapter.onCreateViewHolder(this, AllAppsGridAdapter.VIEW_TYPE_ICON).itemView;
    156         final int iconHeight = icon.getLayoutParams().height;
    157         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_ICON, iconHeight);
    158         mViewHeights.put(AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON, iconHeight);
    159 
    160         final int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
    161                 getResources().getDisplayMetrics().widthPixels, View.MeasureSpec.AT_MOST);
    162         final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(
    163                 getResources().getDisplayMetrics().heightPixels, View.MeasureSpec.AT_MOST);
    164 
    165         putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
    166                 AllAppsGridAdapter.VIEW_TYPE_PREDICTION_DIVIDER,
    167                 AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET_DIVIDER);
    168         putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
    169                 AllAppsGridAdapter.VIEW_TYPE_SEARCH_MARKET);
    170         putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
    171                 AllAppsGridAdapter.VIEW_TYPE_EMPTY_SEARCH);
    172 
    173         if (FeatureFlags.DISCOVERY_ENABLED) {
    174             putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
    175                     AllAppsGridAdapter.VIEW_TYPE_APPS_LOADING_DIVIDER);
    176             putSameHeightFor(adapter, widthMeasureSpec, heightMeasureSpec,
    177                     AllAppsGridAdapter.VIEW_TYPE_DISCOVERY_ITEM);
    178         }
    179     }
    180 
    181     private void putSameHeightFor(AllAppsGridAdapter adapter, int w, int h, int... viewTypes) {
    182         View view = adapter.onCreateViewHolder(this, viewTypes[0]).itemView;
    183         view.measure(w, h);
    184         for (int viewType : viewTypes) {
    185             mViewHeights.put(viewType, view.getMeasuredHeight());
    186         }
    187     }
    188 
    189     /**
    190      * Scrolls this recycler view to the top.
    191      */
    192     public void scrollToTop() {
    193         // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
    194         if (mScrollbar != null) {
    195             mScrollbar.reattachThumbToScroll();
    196         }
    197         scrollToPosition(0);
    198     }
    199 
    200     @Override
    201     public void onDraw(Canvas c) {
    202         // Draw the background
    203         if (mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
    204             mEmptySearchBackground.draw(c);
    205         }
    206 
    207         super.onDraw(c);
    208     }
    209 
    210     @Override
    211     protected void dispatchDraw(Canvas canvas) {
    212         canvas.translate(0, mContentTranslationY);
    213         super.dispatchDraw(canvas);
    214         canvas.translate(0, -mContentTranslationY);
    215     }
    216 
    217     public float getContentTranslationY() {
    218         return mContentTranslationY;
    219     }
    220 
    221     /**
    222      * Use this method instead of calling {@link #setTranslationY(float)}} directly to avoid drawing
    223      * on top of other Views.
    224      */
    225     public void setContentTranslationY(float y) {
    226         mContentTranslationY = y;
    227         invalidate();
    228     }
    229 
    230     @Override
    231     protected boolean verifyDrawable(Drawable who) {
    232         return who == mEmptySearchBackground || super.verifyDrawable(who);
    233     }
    234 
    235     @Override
    236     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    237         updateEmptySearchBackgroundBounds();
    238     }
    239 
    240     @Override
    241     public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
    242         if (mApps.hasFilter()) {
    243             targetParent.containerType = ContainerType.SEARCHRESULT;
    244         } else {
    245             if (v instanceof BubbleTextView) {
    246                 BubbleTextView icon = (BubbleTextView) v;
    247                 int position = getChildPosition(icon);
    248                 if (position != NO_POSITION) {
    249                     List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
    250                     AlphabeticalAppsList.AdapterItem item = items.get(position);
    251                     if (item.viewType == AllAppsGridAdapter.VIEW_TYPE_PREDICTION_ICON) {
    252                         targetParent.containerType = ContainerType.PREDICTION;
    253                         target.predictedRank = item.rowAppIndex;
    254                         return;
    255                     }
    256                 }
    257             }
    258             targetParent.containerType = ContainerType.ALLAPPS;
    259         }
    260     }
    261 
    262     public void onSearchResultsChanged() {
    263         // Always scroll the view to the top so the user can see the changed results
    264         scrollToTop();
    265 
    266         if (mApps.shouldShowEmptySearch()) {
    267             if (mEmptySearchBackground == null) {
    268                 mEmptySearchBackground = DrawableFactory.get(getContext())
    269                         .getAllAppsBackground(getContext());
    270                 mEmptySearchBackground.setAlpha(0);
    271                 mEmptySearchBackground.setCallback(this);
    272                 updateEmptySearchBackgroundBounds();
    273             }
    274             mEmptySearchBackground.animateBgAlpha(1f, 150);
    275         } else if (mEmptySearchBackground != null) {
    276             // For the time being, we just immediately hide the background to ensure that it does
    277             // not overlap with the results
    278             mEmptySearchBackground.setBgAlpha(0f);
    279         }
    280     }
    281 
    282     @Override
    283     public boolean onInterceptTouchEvent(MotionEvent e) {
    284         mPullDetector.onTouchEvent(e);
    285         boolean result = super.onInterceptTouchEvent(e) || mOverScrollHelper.isInOverScroll();
    286         if (!result && e.getAction() == MotionEvent.ACTION_DOWN
    287                 && mEmptySearchBackground != null && mEmptySearchBackground.getAlpha() > 0) {
    288             mEmptySearchBackground.setHotspot(e.getX(), e.getY());
    289         }
    290         return result;
    291     }
    292 
    293     /**
    294      * Maps the touch (from 0..1) to the adapter position that should be visible.
    295      */
    296     @Override
    297     public String scrollToPositionAtProgress(float touchFraction) {
    298         int rowCount = mApps.getNumAppRows();
    299         if (rowCount == 0) {
    300             return "";
    301         }
    302 
    303         // Stop the scroller if it is scrolling
    304         stopScroll();
    305 
    306         // Find the fastscroll section that maps to this touch fraction
    307         List<AlphabeticalAppsList.FastScrollSectionInfo> fastScrollSections =
    308                 mApps.getFastScrollerSections();
    309         AlphabeticalAppsList.FastScrollSectionInfo lastInfo = fastScrollSections.get(0);
    310         for (int i = 1; i < fastScrollSections.size(); i++) {
    311             AlphabeticalAppsList.FastScrollSectionInfo info = fastScrollSections.get(i);
    312             if (info.touchFraction > touchFraction) {
    313                 break;
    314             }
    315             lastInfo = info;
    316         }
    317 
    318         // Update the fast scroll
    319         int scrollY = getCurrentScrollY();
    320         int availableScrollHeight = getAvailableScrollHeight();
    321         mFastScrollHelper.smoothScrollToSection(scrollY, availableScrollHeight, lastInfo);
    322         return lastInfo.sectionName;
    323     }
    324 
    325     @Override
    326     public void onFastScrollCompleted() {
    327         super.onFastScrollCompleted();
    328         mFastScrollHelper.onFastScrollCompleted();
    329     }
    330 
    331     @Override
    332     public void setAdapter(Adapter adapter) {
    333         super.setAdapter(adapter);
    334         adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
    335             public void onChanged() {
    336                 mCachedScrollPositions.clear();
    337             }
    338         });
    339         mFastScrollHelper.onSetAdapter((AllAppsGridAdapter) adapter);
    340     }
    341 
    342     @Override
    343     protected float getBottomFadingEdgeStrength() {
    344         // No bottom fading edge.
    345         return 0;
    346     }
    347 
    348     @Override
    349     protected boolean isPaddingOffsetRequired() {
    350         return true;
    351     }
    352 
    353     @Override
    354     protected int getTopPaddingOffset() {
    355         return -getPaddingTop();
    356     }
    357 
    358     /**
    359      * Updates the bounds for the scrollbar.
    360      */
    361     @Override
    362     public void onUpdateScrollbar(int dy) {
    363         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
    364 
    365         // Skip early if there are no items or we haven't been measured
    366         if (items.isEmpty() || mNumAppsPerRow == 0) {
    367             mScrollbar.setThumbOffsetY(-1);
    368             return;
    369         }
    370 
    371         // Skip early if, there no child laid out in the container.
    372         int scrollY = getCurrentScrollY();
    373         if (scrollY < 0) {
    374             mScrollbar.setThumbOffsetY(-1);
    375             return;
    376         }
    377 
    378         // Only show the scrollbar if there is height to be scrolled
    379         int availableScrollBarHeight = getAvailableScrollBarHeight();
    380         int availableScrollHeight = getAvailableScrollHeight();
    381         if (availableScrollHeight <= 0) {
    382             mScrollbar.setThumbOffsetY(-1);
    383             return;
    384         }
    385 
    386         if (mScrollbar.isThumbDetached()) {
    387             if (!mScrollbar.isDraggingThumb()) {
    388                 // Calculate the current scroll position, the scrollY of the recycler view accounts
    389                 // for the view padding, while the scrollBarY is drawn right up to the background
    390                 // padding (ignoring padding)
    391                 int scrollBarY = (int)
    392                         (((float) scrollY / availableScrollHeight) * availableScrollBarHeight);
    393 
    394                 int thumbScrollY = mScrollbar.getThumbOffsetY();
    395                 int diffScrollY = scrollBarY - thumbScrollY;
    396                 if (diffScrollY * dy > 0f) {
    397                     // User is scrolling in the same direction the thumb needs to catch up to the
    398                     // current scroll position.  We do this by mapping the difference in movement
    399                     // from the original scroll bar position to the difference in movement necessary
    400                     // in the detached thumb position to ensure that both speed towards the same
    401                     // position at either end of the list.
    402                     if (dy < 0) {
    403                         int offset = (int) ((dy * thumbScrollY) / (float) scrollBarY);
    404                         thumbScrollY += Math.max(offset, diffScrollY);
    405                     } else {
    406                         int offset = (int) ((dy * (availableScrollBarHeight - thumbScrollY)) /
    407                                 (float) (availableScrollBarHeight - scrollBarY));
    408                         thumbScrollY += Math.min(offset, diffScrollY);
    409                     }
    410                     thumbScrollY = Math.max(0, Math.min(availableScrollBarHeight, thumbScrollY));
    411                     mScrollbar.setThumbOffsetY(thumbScrollY);
    412                     if (scrollBarY == thumbScrollY) {
    413                         mScrollbar.reattachThumbToScroll();
    414                     }
    415                 } else {
    416                     // User is scrolling in an opposite direction to the direction that the thumb
    417                     // needs to catch up to the scroll position.  Do nothing except for updating
    418                     // the scroll bar x to match the thumb width.
    419                     mScrollbar.setThumbOffsetY(thumbScrollY);
    420                 }
    421             }
    422         } else {
    423             synchronizeScrollBarThumbOffsetToViewScroll(scrollY, availableScrollHeight);
    424         }
    425     }
    426 
    427     @Override
    428     public boolean supportsFastScrolling() {
    429         // Only allow fast scrolling when the user is not searching, since the results are not
    430         // grouped in a meaningful order
    431         return !mApps.hasFilter();
    432     }
    433 
    434     @Override
    435     public int getCurrentScrollY() {
    436         // Return early if there are no items or we haven't been measured
    437         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
    438         if (items.isEmpty() || mNumAppsPerRow == 0 || getChildCount() == 0) {
    439             return -1;
    440         }
    441 
    442         // Calculate the y and offset for the item
    443         View child = getChildAt(0);
    444         int position = getChildPosition(child);
    445         if (position == NO_POSITION) {
    446             return -1;
    447         }
    448         return getPaddingTop() +
    449                 getCurrentScrollY(position, getLayoutManager().getDecoratedTop(child));
    450     }
    451 
    452     public int getCurrentScrollY(int position, int offset) {
    453         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
    454         AlphabeticalAppsList.AdapterItem posItem = position < items.size() ?
    455                 items.get(position) : null;
    456         int y = mCachedScrollPositions.get(position, -1);
    457         if (y < 0) {
    458             y = 0;
    459             for (int i = 0; i < position; i++) {
    460                 AlphabeticalAppsList.AdapterItem item = items.get(i);
    461                 if (AllAppsGridAdapter.isIconViewType(item.viewType)) {
    462                     // Break once we reach the desired row
    463                     if (posItem != null && posItem.viewType == item.viewType &&
    464                             posItem.rowIndex == item.rowIndex) {
    465                         break;
    466                     }
    467                     // Otherwise, only account for the first icon in the row since they are the same
    468                     // size within a row
    469                     if (item.rowAppIndex == 0) {
    470                         y += mViewHeights.get(item.viewType, 0);
    471                     }
    472                 } else {
    473                     // Rest of the views span the full width
    474                     y += mViewHeights.get(item.viewType, 0);
    475                 }
    476             }
    477             mCachedScrollPositions.put(position, y);
    478         }
    479         return y - offset;
    480     }
    481 
    482     /**
    483      * Returns the available scroll height:
    484      *   AvailableScrollHeight = Total height of the all items - last page height
    485      */
    486     @Override
    487     protected int getAvailableScrollHeight() {
    488         return getPaddingTop() + getCurrentScrollY(mApps.getAdapterItems().size(), 0)
    489                 - getHeight() + getPaddingBottom();
    490     }
    491 
    492     /**
    493      * Updates the bounds of the empty search background.
    494      */
    495     private void updateEmptySearchBackgroundBounds() {
    496         if (mEmptySearchBackground == null) {
    497             return;
    498         }
    499 
    500         // Center the empty search background on this new view bounds
    501         int x = (getMeasuredWidth() - mEmptySearchBackground.getIntrinsicWidth()) / 2;
    502         int y = mEmptySearchBackgroundTopOffset;
    503         mEmptySearchBackground.setBounds(x, y,
    504                 x + mEmptySearchBackground.getIntrinsicWidth(),
    505                 y + mEmptySearchBackground.getIntrinsicHeight());
    506     }
    507 
    508     private class SpringMotionOnScrollListener extends RecyclerView.OnScrollListener {
    509 
    510         @Override
    511         public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    512             if (mOverScrollHelper.isInOverScroll()) {
    513                 // OverScroll will handle animating the springs.
    514                 return;
    515             }
    516 
    517             // We only start the spring animation when we hit the top/bottom, to ensure
    518             // that all of the animations start at the same time.
    519             if (dy < 0 && !canScrollVertically(-1)) {
    520                 mSpringAnimationHandler.animateToFinalPosition(0, 1);
    521             } else if (dy > 0 && !canScrollVertically(1)) {
    522                 mSpringAnimationHandler.animateToFinalPosition(0, -1);
    523             }
    524         }
    525     }
    526 
    527     private class OverScrollHelper implements SwipeDetector.Listener {
    528 
    529         private static final float MAX_RELEASE_VELOCITY = 5000; // px / s
    530         private static final float MAX_OVERSCROLL_PERCENTAGE = 0.07f;
    531 
    532         private boolean mIsInOverScroll;
    533 
    534         // We use this value to calculate the actual amount the user has overscrolled.
    535         private float mFirstDisplacement = 0;
    536 
    537         private boolean mAlreadyScrollingUp;
    538         private int mFirstScrollYOnScrollUp;
    539 
    540         @Override
    541         public void onDragStart(boolean start) {
    542         }
    543 
    544         @Override
    545         public boolean onDrag(float displacement, float velocity) {
    546             boolean isScrollingUp = displacement > 0;
    547             if (isScrollingUp) {
    548                 if (!mAlreadyScrollingUp) {
    549                     mFirstScrollYOnScrollUp = getCurrentScrollY();
    550                     mAlreadyScrollingUp = true;
    551                 }
    552             } else {
    553                 mAlreadyScrollingUp = false;
    554             }
    555 
    556             // Only enter overscroll if the user is interacting with the RecyclerView directly
    557             // and if one of the following criteria are met:
    558             // - User scrolls down when they're already at the bottom.
    559             // - User starts scrolling up, hits the top, and continues scrolling up.
    560             boolean wasInOverScroll = mIsInOverScroll;
    561             mIsInOverScroll = !mScrollbar.isDraggingThumb() &&
    562                     ((!canScrollVertically(1) && displacement < 0) ||
    563                     (!canScrollVertically(-1) && isScrollingUp && mFirstScrollYOnScrollUp != 0));
    564 
    565             if (wasInOverScroll && !mIsInOverScroll) {
    566                 // Exit overscroll. This can happen when the user is in overscroll and then
    567                 // scrolls the opposite way.
    568                 reset(false /* shouldSpring */);
    569             } else if (mIsInOverScroll) {
    570                 if (Float.compare(mFirstDisplacement, 0) == 0) {
    571                     // Because users can scroll before entering overscroll, we need to
    572                     // subtract the amount where the user was not in overscroll.
    573                     mFirstDisplacement = displacement;
    574                 }
    575                 float overscrollY = displacement - mFirstDisplacement;
    576                 setContentTranslationY(getDampedOverScroll(overscrollY));
    577             }
    578 
    579             return mIsInOverScroll;
    580         }
    581 
    582         @Override
    583         public void onDragEnd(float velocity, boolean fling) {
    584            reset(mIsInOverScroll  /* shouldSpring */);
    585         }
    586 
    587         private void reset(boolean shouldSpring) {
    588             float y = getContentTranslationY();
    589             if (Float.compare(y, 0) != 0) {
    590                 if (FeatureFlags.LAUNCHER3_PHYSICS && shouldSpring) {
    591                     // We calculate our own velocity to give the springs the desired effect.
    592                     float velocity = y / getDampedOverScroll(getHeight()) * MAX_RELEASE_VELOCITY;
    593                     // We want to negate the velocity because we are moving to 0 from -1 due to the
    594                     // downward motion. (y-axis -1 is above 0).
    595                     mSpringAnimationHandler.animateToPositionWithVelocity(0, -1, -velocity);
    596                 }
    597 
    598                 ObjectAnimator.ofFloat(AllAppsRecyclerView.this,
    599                         AllAppsRecyclerView.CONTENT_TRANS_Y, 0)
    600                         .setDuration(100)
    601                         .start();
    602             }
    603             mIsInOverScroll = false;
    604             mFirstDisplacement = 0;
    605             mFirstScrollYOnScrollUp = 0;
    606             mAlreadyScrollingUp = false;
    607         }
    608 
    609         public boolean isInOverScroll() {
    610             return mIsInOverScroll;
    611         }
    612 
    613         private float getDampedOverScroll(float y) {
    614             return OverScroll.dampedScroll(y, getHeight());
    615         }
    616     }
    617 }
    618