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