From 89d5c5a31bd6cf4caf815b680ec670896b91803d Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Fri, 23 Jun 2017 16:12:50 -0700 Subject: Updating fast scrollbar UI in Landscape Creating a separate view for FastScrollBar and moving all the relavant logic in the view. For protrait, the touch handling is delegated by the recycler view just like before. For landscape, the dcrollbar does not overlay with recyclerView and handles the touch itself Bug: 37015359 Change-Id: Ie1981326457ba739bdf0ac8063db1065f395f133 --- src/com/android/launcher3/BaseRecyclerView.java | 116 ++----- .../launcher3/BaseRecyclerViewFastScrollBar.java | 314 ------------------ .../launcher3/allapps/AllAppsContainerView.java | 12 +- .../launcher3/allapps/AllAppsGridAdapter.java | 14 +- .../launcher3/allapps/AllAppsRecyclerView.java | 10 +- .../launcher3/allapps/AlphabeticalAppsList.java | 18 -- .../launcher3/allapps/LandscapeFastScroller.java | 63 ++++ .../allapps/search/AppsSearchContainerLayout.java | 12 +- .../allapps/search/HeaderElevationController.java | 32 +- .../launcher3/views/RecyclerViewFastScroller.java | 356 +++++++++++++++++++++ 10 files changed, 486 insertions(+), 461 deletions(-) delete mode 100644 src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java create mode 100644 src/com/android/launcher3/allapps/LandscapeFastScroller.java create mode 100644 src/com/android/launcher3/views/RecyclerViewFastScroller.java (limited to 'src/com/android') diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java index 84358ea37..3ee6e51b8 100644 --- a/src/com/android/launcher3/BaseRecyclerView.java +++ b/src/com/android/launcher3/BaseRecyclerView.java @@ -22,8 +22,9 @@ import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ViewGroup; +import android.widget.TextView; -import com.android.launcher3.util.Thunk; +import com.android.launcher3.views.RecyclerViewFastScroller; /** @@ -36,19 +37,7 @@ import com.android.launcher3.util.Thunk; public abstract class BaseRecyclerView extends RecyclerView implements RecyclerView.OnItemTouchListener { - private static final int SCROLL_DELTA_THRESHOLD_DP = 4; - - /** Keeps the last known scrolling delta/velocity along y-axis. */ - @Thunk int mDy = 0; - private float mDeltaThreshold; - - protected final BaseRecyclerViewFastScrollBar mScrollbar; - - private int mDownX; - private int mDownY; - private int mLastY; - - private boolean mScrollBarVisible = true; + protected RecyclerViewFastScroller mScrollbar; public BaseRecyclerView(Context context) { this(context, null); @@ -60,28 +49,6 @@ public abstract class BaseRecyclerView extends RecyclerView public BaseRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - mDeltaThreshold = getResources().getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; - mScrollbar = new BaseRecyclerViewFastScrollBar(this, getResources()); - - ScrollListener listener = new ScrollListener(); - setOnScrollListener(listener); - } - - private class ScrollListener extends OnScrollListener { - public ScrollListener() { - // Do nothing - } - - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - mDy = dy; - - // TODO(winsonc): If we want to animate the section heads while scrolling, we can - // initiate that here if the recycler view scroll state is not - // RecyclerView.SCROLL_STATE_IDLE. - - onUpdateScrollbar(dy); - } } @Override @@ -93,7 +60,9 @@ public abstract class BaseRecyclerView extends RecyclerView @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - mScrollbar.setPopupView(((ViewGroup) getParent()).findViewById(R.id.fast_scroller_popup)); + ViewGroup parent = (ViewGroup) getParent(); + mScrollbar = parent.findViewById(R.id.fast_scroller); + mScrollbar.setRecyclerView(this, (TextView) parent.findViewById(R.id.fast_scroller_popup)); } /** @@ -115,57 +84,25 @@ public abstract class BaseRecyclerView extends RecyclerView * it is already showing). */ private boolean handleTouchEvent(MotionEvent ev) { - ev.offsetLocation(0, -getPaddingTop()); - int action = ev.getAction(); - int x = (int) ev.getX(); - int y = (int) ev.getY(); - switch (action) { - case MotionEvent.ACTION_DOWN: - // Keep track of the down positions - mDownX = x; - mDownY = mLastY = y; - if (shouldStopScroll(ev)) { - stopScroll(); - } - mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); - break; - case MotionEvent.ACTION_MOVE: - mLastY = y; - mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - onFastScrollCompleted(); - mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY); - break; + // Move to mScrollbar's coordinate system. + int left = getLeft() - mScrollbar.getLeft(); + int top = getTop() - mScrollbar.getTop(); + ev.offsetLocation(left, top); + try { + return mScrollbar.handleTouchEvent(ev); + } finally { + ev.offsetLocation(-left, -top); } - ev.offsetLocation(0, getPaddingTop()); - return mScrollbar.isDraggingThumb(); } public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { // DO NOT REMOVE, NEEDED IMPLEMENTATION FOR M BUILDS } - /** - * Returns whether this {@link MotionEvent} should trigger the scroll to be stopped. - */ - protected boolean shouldStopScroll(MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - if ((Math.abs(mDy) < mDeltaThreshold && - getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { - // now the touch events are being passed to the {@link WidgetCell} until the - // touch sequence goes over the touch slop. - return true; - } - } - return false; - } - /** * Returns the height of the fast scroll bar */ - protected int getScrollbarTrackHeight() { + public int getScrollbarTrackHeight() { return getHeight() - getPaddingTop() - getPaddingBottom(); } @@ -187,25 +124,14 @@ public abstract class BaseRecyclerView extends RecyclerView /** * Returns the scrollbar for this recycler view. */ - public BaseRecyclerViewFastScrollBar getScrollBar() { + public RecyclerViewFastScroller getScrollBar() { return mScrollbar; } @Override protected void dispatchDraw(Canvas canvas) { + onUpdateScrollbar(0); super.dispatchDraw(canvas); - if (mScrollBarVisible) { - onUpdateScrollbar(0); - mScrollbar.draw(canvas); - } - } - - /** - * Sets the scrollbar visibility. The call does not refresh the UI, its the responsibility - * of the caller to call {@link #invalidate()}. - */ - public void setScrollBarVisible(boolean visible) { - mScrollBarVisible = visible; } /** @@ -236,7 +162,7 @@ public abstract class BaseRecyclerView extends RecyclerView /** * @return whether fast scrolling is supported in the current state. */ - protected boolean supportsFastScrolling() { + public boolean supportsFastScrolling() { return true; } @@ -252,16 +178,16 @@ public abstract class BaseRecyclerView extends RecyclerView * Maps the touch (from 0..1) to the adapter position that should be visible. *

Override in each subclass of this base class. */ - protected abstract String scrollToPositionAtProgress(float touchFraction); + public abstract String scrollToPositionAtProgress(float touchFraction); /** * Updates the bounds for the scrollbar. *

Override in each subclass of this base class. */ - protected abstract void onUpdateScrollbar(int dy); + public abstract void onUpdateScrollbar(int dy); /** *

Override in each subclass of this base class. */ - protected void onFastScrollCompleted() {} + public void onFastScrollCompleted() {} } \ No newline at end of file diff --git a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java b/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java deleted file mode 100644 index 9e8d300ae..000000000 --- a/src/com/android/launcher3/BaseRecyclerViewFastScrollBar.java +++ /dev/null @@ -1,314 +0,0 @@ -/* - * 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; - -import android.animation.ObjectAnimator; -import android.content.res.Resources; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.graphics.Rect; -import android.util.Property; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewConfiguration; -import android.widget.TextView; - -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.graphics.FastScrollThumbDrawable; -import com.android.launcher3.util.Themes; - -/** - * The track and scrollbar that shows when you scroll the list. - */ -public class BaseRecyclerViewFastScrollBar { - - private static final Property TRACK_WIDTH = - new Property(Integer.class, "width") { - - @Override - public Integer get(BaseRecyclerViewFastScrollBar scrollBar) { - return scrollBar.mWidth; - } - - @Override - public void set(BaseRecyclerViewFastScrollBar scrollBar, Integer value) { - scrollBar.setTrackWidth(value); - } - }; - - private final static int MAX_TRACK_ALPHA = 30; - private final static int SCROLL_BAR_VIS_DURATION = 150; - private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 0.75f; - - private final Rect mTmpRect = new Rect(); - private final BaseRecyclerView mRv; - - private final boolean mIsRtl; - - // The inset is the buffer around which a point will still register as a click on the scrollbar - private final int mTouchInset; - - private final int mMinWidth; - private final int mMaxWidth; - private final int mThumbPadding; - - // Current width of the track - private int mWidth; - private ObjectAnimator mWidthAnimator; - - private final Paint mThumbPaint; - private final int mThumbHeight; - - private final Paint mTrackPaint; - - private float mLastTouchY; - private boolean mIsDragging; - private boolean mIsThumbDetached; - private boolean mCanThumbDetach; - private boolean mIgnoreDragGesture; - - // This is the offset from the top of the scrollbar when the user first starts touching. To - // prevent jumping, this offset is applied as the user scrolls. - private int mTouchOffsetY; - private int mThumbOffsetY; - - // Fast scroller popup - private TextView mPopupView; - private boolean mPopupVisible; - private String mPopupSectionName; - - public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) { - mRv = rv; - mTrackPaint = new Paint(); - mTrackPaint.setColor(Themes.getAttrColor(rv.getContext(), android.R.attr.textColorPrimary)); - mTrackPaint.setAlpha(MAX_TRACK_ALPHA); - - mThumbPaint = new Paint(); - mThumbPaint.setAntiAlias(true); - mThumbPaint.setColor(Themes.getColorAccent(rv.getContext())); - mThumbPaint.setStyle(Paint.Style.FILL); - - mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_min_width); - mMaxWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_max_width); - - mThumbPadding = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_padding); - mThumbHeight = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height); - - mTouchInset = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_touch_inset); - mIsRtl = Utilities.isRtl(res); - } - - public void setPopupView(View popup) { - mPopupView = (TextView) popup; - mPopupView.setBackground(new FastScrollThumbDrawable(mThumbPaint, mIsRtl)); - } - - public void setDetachThumbOnFastScroll() { - mCanThumbDetach = true; - } - - public void reattachThumbToScroll() { - mIsThumbDetached = false; - } - - private int getDrawLeft() { - return mIsRtl ? 0 : (mRv.getWidth() - mMaxWidth); - } - - public void setThumbOffsetY(int y) { - if (mThumbOffsetY == y) { - return; - } - - // Invalidate the previous and new thumb area - int drawLeft = getDrawLeft(); - mTmpRect.set(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight); - mThumbOffsetY = y; - mTmpRect.union(drawLeft, mThumbOffsetY, drawLeft + mMaxWidth, mThumbOffsetY + mThumbHeight); - mTmpRect.offset(0, mRv.getPaddingTop()); - mRv.invalidate(mTmpRect); - } - - public int getThumbOffsetY() { - return mThumbOffsetY; - } - - private void setTrackWidth(int width) { - if (mWidth == width) { - return; - } - int left = getDrawLeft(); - int top = mRv.getPaddingTop(); - // Invalidate the whole scroll bar area. - mRv.invalidate(left, top, left + mMaxWidth, top + mRv.getScrollbarTrackHeight()); - - mWidth = width; - } - - public int getThumbHeight() { - return mThumbHeight; - } - - public boolean isDraggingThumb() { - return mIsDragging; - } - - public boolean isThumbDetached() { - return mIsThumbDetached; - } - - /** - * Handles the touch event and determines whether to show the fast scroller (or updates it if - * it is already showing). - */ - public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY) { - ViewConfiguration config = ViewConfiguration.get(mRv.getContext()); - - int action = ev.getAction(); - int y = (int) ev.getY(); - switch (action) { - case MotionEvent.ACTION_DOWN: - if (isNearThumb(downX, downY)) { - mTouchOffsetY = downY - mThumbOffsetY; - } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL - && mRv.supportsFastScrolling() - && isNearScrollBar(downX)) { - calcTouchOffsetAndPrepToFastScroll(downY, lastY); - updateFastScrollSectionNameAndThumbOffset(lastY, y); - } - break; - case MotionEvent.ACTION_MOVE: - // Check if we should start scrolling, but ignore this fastscroll gesture if we have - // exceeded some fixed movement - mIgnoreDragGesture |= Math.abs(y - downY) > config.getScaledPagingTouchSlop(); - if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() && - isNearThumb(downX, lastY) && - Math.abs(y - downY) > config.getScaledTouchSlop()) { - calcTouchOffsetAndPrepToFastScroll(downY, lastY); - } - if (mIsDragging) { - updateFastScrollSectionNameAndThumbOffset(lastY, y); - } - break; - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - mTouchOffsetY = 0; - mLastTouchY = 0; - mIgnoreDragGesture = false; - if (mIsDragging) { - mIsDragging = false; - animatePopupVisibility(false); - showActiveScrollbar(false); - } - break; - } - } - - private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) { - mRv.getParent().requestDisallowInterceptTouchEvent(true); - mIsDragging = true; - if (mCanThumbDetach) { - mIsThumbDetached = true; - } - mTouchOffsetY += (lastY - downY); - animatePopupVisibility(true); - showActiveScrollbar(true); - } - - private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) { - // Update the fastscroller section name at this touch position - int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight; - float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY)); - String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom); - if (!sectionName.equals(mPopupSectionName)) { - mPopupSectionName = sectionName; - mPopupView.setText(sectionName); - } - animatePopupVisibility(!sectionName.isEmpty()); - updatePopupY(lastY); - mLastTouchY = boundedY; - setThumbOffsetY((int) mLastTouchY); - } - - public void draw(Canvas canvas) { - if (mThumbOffsetY < 0) { - return; - } - int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); - if (!mIsRtl) { - canvas.translate(mRv.getWidth() - mWidth, 0); - } - canvas.translate(0, mRv.getPaddingTop()); - // Draw the track - canvas.drawRoundRect(0, 0, mWidth, mRv.getScrollbarTrackHeight(), - mWidth, mWidth, mTrackPaint); - - canvas.translate(-mThumbPadding, mThumbOffsetY); - float r = mWidth + mThumbPadding + mThumbPadding; - canvas.drawRoundRect(0, 0, r, mThumbHeight, r, r, mThumbPaint); - canvas.restoreToCount(saveCount); - } - - /** - * Animates the width of the scrollbar. - */ - private void showActiveScrollbar(boolean isScrolling) { - if (mWidthAnimator != null) { - mWidthAnimator.cancel(); - } - - mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH, - isScrolling ? mMaxWidth : mMinWidth); - mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION); - mWidthAnimator.start(); - } - - /** - * Returns whether the specified point is inside the thumb bounds. - */ - public boolean isNearThumb(int x, int y) { - int left = getDrawLeft(); - mTmpRect.set(left, mThumbOffsetY, left + mMaxWidth, mThumbOffsetY + mThumbHeight); - mTmpRect.inset(mTouchInset, mTouchInset); - return mTmpRect.contains(x, y); - } - - /** - * Returns whether the specified x position is near the scroll bar. - */ - public boolean isNearScrollBar(int x) { - int left = getDrawLeft(); - return x >= left && x <= left + mMaxWidth; - } - - private void animatePopupVisibility(boolean visible) { - if (mPopupVisible != visible) { - mPopupVisible = visible; - mPopupView.animate().cancel(); - mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start(); - } - } - - private void updatePopupY(int lastTouchY) { - int height = mPopupView.getHeight(); - float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height); - top = Utilities.boundToRange(top, - mMaxWidth, mRv.getScrollbarTrackHeight() - mMaxWidth - height); - mPopupView.setTranslationY(top); - } -} diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java index 4954e0c44..47b68a2ee 100644 --- a/src/com/android/launcher3/allapps/AllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java @@ -171,19 +171,19 @@ public class AllAppsContainerView extends BaseContainerView implements DragSourc * Returns whether the view itself will handle the touch event or not. */ public boolean shouldContainerScroll(MotionEvent ev) { - int[] point = new int[2]; - point[0] = (int) ev.getX(); - point[1] = (int) ev.getY(); - Utilities.mapCoordInSelfToDescendant(mAppsRecyclerView, this, point); - // IF the MotionEvent is inside the search box, and the container keeps on receiving // touch input, container should move down. if (mLauncher.getDragLayer().isEventOverView(mSearchContainer, ev)) { return true; } + int[] point = new int[2]; + point[0] = (int) ev.getX(); + point[1] = (int) ev.getY(); + Utilities.mapCoordInSelfToDescendant( + mAppsRecyclerView.getScrollBar(), mLauncher.getDragLayer(), point); // IF the MotionEvent is inside the thumb, container should not be pulled down. - if (mAppsRecyclerView.getScrollBar().isNearThumb(point[0], point[1])) { + if (mAppsRecyclerView.getScrollBar().shouldBlockIntercept(point[0], point[1])) { return false; } diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index d6514a83f..1054a5633 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -69,16 +69,13 @@ public class AllAppsGridAdapter extends RecyclerView.Adapter= 0 && x < getWidth() && y >= 0 && y < getHeight(); + } +} diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java index 3f06ec9dd..5cb12d592 100644 --- a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java +++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java @@ -54,12 +54,13 @@ public class AppsSearchContainerLayout extends FrameLayout private final int mSearchBoxHeight; private final AllAppsSearchBarController mSearchBarController; private final SpannableStringBuilder mSearchQueryBuilder; - private final HeaderElevationController mElevationController; private ExtendedEditText mSearchInput; private AlphabeticalAppsList mApps; private AllAppsRecyclerView mAppsRecyclerView; private AllAppsGridAdapter mAdapter; + private View mDivider; + private HeaderElevationController mElevationController; public AppsSearchContainerLayout(Context context) { this(context, null); @@ -77,7 +78,6 @@ public class AppsSearchContainerLayout extends FrameLayout mSearchBoxHeight = getResources() .getDimensionPixelSize(R.dimen.all_apps_search_bar_field_height); mSearchBarController = new AllAppsSearchBarController(); - mElevationController = new HeaderElevationController(this); mSearchQueryBuilder = new SpannableStringBuilder(); Selection.setSelection(mSearchQueryBuilder, 0); @@ -87,6 +87,8 @@ public class AppsSearchContainerLayout extends FrameLayout protected void onFinishInflate() { super.onFinishInflate(); mSearchInput = findViewById(R.id.search_box_input); + mDivider = findViewById(R.id.search_divider); + mElevationController = new HeaderElevationController(mDivider); // Update the hint to contain the icon. // Prefix the original hint with two spaces. The first space gets replaced by the icon @@ -96,6 +98,12 @@ public class AppsSearchContainerLayout extends FrameLayout spanned.setSpan(new TintedDrawableSpan(getContext(), R.drawable.ic_allapps_search), 0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); mSearchInput.setHint(spanned); + + DeviceProfile dp = mLauncher.getDeviceProfile(); + if (!dp.isVerticalBarLayout()) { + LayoutParams lp = (LayoutParams) mDivider.getLayoutParams(); + lp.leftMargin = lp.rightMargin = dp.edgeMarginPx; + } } @Override diff --git a/src/com/android/launcher3/allapps/search/HeaderElevationController.java b/src/com/android/launcher3/allapps/search/HeaderElevationController.java index ab4e88fc8..7cd32b26e 100644 --- a/src/com/android/launcher3/allapps/search/HeaderElevationController.java +++ b/src/com/android/launcher3/allapps/search/HeaderElevationController.java @@ -4,11 +4,11 @@ import android.content.res.Resources; import android.graphics.Outline; import android.support.v7.widget.RecyclerView; import android.view.View; +import android.view.ViewGroup; import android.view.ViewOutlineProvider; import com.android.launcher3.BaseRecyclerView; import com.android.launcher3.R; -import com.android.launcher3.Utilities; /** * Helper class for controlling the header elevation in response to RecyclerView scroll. @@ -16,6 +16,7 @@ import com.android.launcher3.Utilities; public class HeaderElevationController extends RecyclerView.OnScrollListener { private final View mHeader; + private final View mHeaderChild; private final float mMaxElevation; private final float mScrollToElevation; @@ -28,23 +29,27 @@ public class HeaderElevationController extends RecyclerView.OnScrollListener { mScrollToElevation = res.getDimension(R.dimen.all_apps_header_scroll_to_elevation); // We need to provide a custom outline so the shadow only appears on the bottom edge. - // The top, left and right edges are all extended out, and the shadow is clipped - // by the parent. + // The top, left and right edges are all extended out to match parent's edge, so that + // the shadow is clipped by the parent. final ViewOutlineProvider vop = new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { - final View parent = (View) mHeader.getParent(); + // Set the left and top to be at the parents edge. Since the coordinates are + // relative to this view, + // (x = -view.getLeft()) for this view => (x = 0) for parent + final int left = -view.getLeft(); + final int top = -view.getTop(); - final int left = parent.getLeft(); // Use the parent to account for offsets - final int top = view.getTop(); - final int right = left + view.getWidth(); - final int bottom = view.getBottom(); - - final int offset = Utilities.pxFromDp(mMaxElevation, res.getDisplayMetrics()); + // Since the view is centered align, the spacing on left and right are same. + // Add same spacing on the right to reach parent's edge. + final int right = view.getWidth() - left; + final int bottom = view.getHeight(); + final int offset = (int) mMaxElevation; outline.setRect(left - offset, top - offset, right + offset, bottom); } }; mHeader.setOutlineProvider(vop); + mHeaderChild = ((ViewGroup) mHeader).getChildAt(0); } public void reset() { @@ -63,6 +68,13 @@ public class HeaderElevationController extends RecyclerView.OnScrollListener { float newElevation = mMaxElevation * elevationPct; if (Float.compare(mHeader.getElevation(), newElevation) != 0) { mHeader.setElevation(newElevation); + + // To simulate a scrolling effect for the header, we translate the header down, and + // its content up by the same amount, so that it gets clipped by the parent, making it + // look like the content was scrolled out of the view. + int shift = Math.min(mHeader.getHeight(), scrollY); + mHeader.setTranslationY(-shift); + mHeaderChild.setTranslationY(shift); } } diff --git a/src/com/android/launcher3/views/RecyclerViewFastScroller.java b/src/com/android/launcher3/views/RecyclerViewFastScroller.java new file mode 100644 index 000000000..7b5bcdbd4 --- /dev/null +++ b/src/com/android/launcher3/views/RecyclerViewFastScroller.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.launcher3.views; + +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.Property; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.TextView; + +import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.graphics.FastScrollThumbDrawable; +import com.android.launcher3.util.Themes; + +/** + * The track and scrollbar that shows when you scroll the list. + */ +public class RecyclerViewFastScroller extends View { + + private static final int SCROLL_DELTA_THRESHOLD_DP = 4; + + private static final Property TRACK_WIDTH = + new Property(Integer.class, "width") { + + @Override + public Integer get(RecyclerViewFastScroller scrollBar) { + return scrollBar.mWidth; + } + + @Override + public void set(RecyclerViewFastScroller scrollBar, Integer value) { + scrollBar.setTrackWidth(value); + } + }; + + private final static int MAX_TRACK_ALPHA = 30; + private final static int SCROLL_BAR_VIS_DURATION = 150; + private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 0.75f; + + private final int mMinWidth; + private final int mMaxWidth; + private final int mThumbPadding; + + /** Keeps the last known scrolling delta/velocity along y-axis. */ + private int mDy = 0; + private final float mDeltaThreshold; + + private final ViewConfiguration mConfig; + + // Current width of the track + private int mWidth; + private ObjectAnimator mWidthAnimator; + + private final Paint mThumbPaint; + protected final int mThumbHeight; + + private final Paint mTrackPaint; + + private float mLastTouchY; + private boolean mIsDragging; + private boolean mIsThumbDetached; + private final boolean mCanThumbDetach; + private boolean mIgnoreDragGesture; + + // This is the offset from the top of the scrollbar when the user first starts touching. To + // prevent jumping, this offset is applied as the user scrolls. + protected int mTouchOffsetY; + protected int mThumbOffsetY; + + // Fast scroller popup + private TextView mPopupView; + private boolean mPopupVisible; + private String mPopupSectionName; + + protected BaseRecyclerView mRv; + + private int mDownX; + private int mDownY; + private int mLastY; + + public RecyclerViewFastScroller(Context context) { + this(context, null); + } + + public RecyclerViewFastScroller(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RecyclerViewFastScroller(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + mTrackPaint = new Paint(); + mTrackPaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary)); + mTrackPaint.setAlpha(MAX_TRACK_ALPHA); + + mThumbPaint = new Paint(); + mThumbPaint.setAntiAlias(true); + mThumbPaint.setColor(Themes.getColorAccent(context)); + mThumbPaint.setStyle(Paint.Style.FILL); + + Resources res = getResources(); + mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_min_width); + mMaxWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_max_width); + + mThumbPadding = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_padding); + mThumbHeight = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height); + + mConfig = ViewConfiguration.get(context); + mDeltaThreshold = res.getDisplayMetrics().density * SCROLL_DELTA_THRESHOLD_DP; + + TypedArray ta = + context.obtainStyledAttributes(attrs, R.styleable.RecyclerViewFastScroller, defStyleAttr, 0); + mCanThumbDetach = ta.getBoolean(R.styleable.RecyclerViewFastScroller_canThumbDetach, false); + ta.recycle(); + } + + public void setRecyclerView(BaseRecyclerView rv, TextView popupView) { + mRv = rv; + mRv.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + mDy = dy; + + // TODO(winsonc): If we want to animate the section heads while scrolling, we can + // initiate that here if the recycler view scroll state is not + // RecyclerView.SCROLL_STATE_IDLE. + + mRv.onUpdateScrollbar(dy); + } + }); + + mPopupView = popupView; + mPopupView.setBackground( + new FastScrollThumbDrawable(mThumbPaint, Utilities.isRtl(getResources()))); + } + + public void reattachThumbToScroll() { + mIsThumbDetached = false; + } + + public void setThumbOffsetY(int y) { + if (mThumbOffsetY == y) { + return; + } + mThumbOffsetY = y; + invalidate(); + } + + public int getThumbOffsetY() { + return mThumbOffsetY; + } + + private void setTrackWidth(int width) { + if (mWidth == width) { + return; + } + mWidth = width; + invalidate(); + } + + public int getThumbHeight() { + return mThumbHeight; + } + + public boolean isDraggingThumb() { + return mIsDragging; + } + + public boolean isThumbDetached() { + return mIsThumbDetached; + } + + /** + * Handles the touch event and determines whether to show the fast scroller (or updates it if + * it is already showing). + */ + public boolean handleTouchEvent(MotionEvent ev) { + int x = (int) ev.getX(); + int y = (int) ev.getY(); + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + // Keep track of the down positions + mDownX = x; + mDownY = mLastY = y; + + if ((Math.abs(mDy) < mDeltaThreshold && + mRv.getScrollState() != RecyclerView.SCROLL_STATE_IDLE)) { + // now the touch events are being passed to the {@link WidgetCell} until the + // touch sequence goes over the touch slop. + mRv.stopScroll(); + } + if (isNearThumb(x, y)) { + mTouchOffsetY = mDownY - mThumbOffsetY; + } else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL + && mRv.supportsFastScrolling() + && isNearScrollBar(mDownX)) { + calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY); + updateFastScrollSectionNameAndThumbOffset(mLastY, y); + } + break; + case MotionEvent.ACTION_MOVE: + mLastY = y; + + // Check if we should start scrolling, but ignore this fastscroll gesture if we have + // exceeded some fixed movement + mIgnoreDragGesture |= Math.abs(y - mDownY) > mConfig.getScaledPagingTouchSlop(); + if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() && + isNearThumb(mDownX, mLastY) && + Math.abs(y - mDownY) > mConfig.getScaledTouchSlop()) { + calcTouchOffsetAndPrepToFastScroll(mDownY, mLastY); + } + if (mIsDragging) { + updateFastScrollSectionNameAndThumbOffset(mLastY, y); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mRv.onFastScrollCompleted(); + mTouchOffsetY = 0; + mLastTouchY = 0; + mIgnoreDragGesture = false; + if (mIsDragging) { + mIsDragging = false; + animatePopupVisibility(false); + showActiveScrollbar(false); + } + break; + } + return mIsDragging; + } + + private void calcTouchOffsetAndPrepToFastScroll(int downY, int lastY) { + mRv.getParent().requestDisallowInterceptTouchEvent(true); + mIsDragging = true; + if (mCanThumbDetach) { + mIsThumbDetached = true; + } + mTouchOffsetY += (lastY - downY); + animatePopupVisibility(true); + showActiveScrollbar(true); + } + + private void updateFastScrollSectionNameAndThumbOffset(int lastY, int y) { + // Update the fastscroller section name at this touch position + int bottom = mRv.getScrollbarTrackHeight() - mThumbHeight; + float boundedY = (float) Math.max(0, Math.min(bottom, y - mTouchOffsetY)); + String sectionName = mRv.scrollToPositionAtProgress(boundedY / bottom); + if (!sectionName.equals(mPopupSectionName)) { + mPopupSectionName = sectionName; + mPopupView.setText(sectionName); + } + animatePopupVisibility(!sectionName.isEmpty()); + updatePopupY(lastY); + mLastTouchY = boundedY; + setThumbOffsetY((int) mLastTouchY); + } + + public void onDraw(Canvas canvas) { + if (mThumbOffsetY < 0) { + return; + } + int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.translate(getWidth() / 2, mRv.getPaddingTop()); + // Draw the track + float halfW = mWidth / 2; + canvas.drawRoundRect(-halfW, 0, halfW, mRv.getScrollbarTrackHeight(), + mWidth, mWidth, mTrackPaint); + + canvas.translate(0, mThumbOffsetY); + halfW += mThumbPadding; + float r = mWidth + mThumbPadding + mThumbPadding; + canvas.drawRoundRect(-halfW, 0, halfW, mThumbHeight, r, r, mThumbPaint); + canvas.restoreToCount(saveCount); + } + + + /** + * Animates the width of the scrollbar. + */ + private void showActiveScrollbar(boolean isScrolling) { + if (mWidthAnimator != null) { + mWidthAnimator.cancel(); + } + + mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH, + isScrolling ? mMaxWidth : mMinWidth); + mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION); + mWidthAnimator.start(); + } + + /** + * Returns whether the specified point is inside the thumb bounds. + */ + private boolean isNearThumb(int x, int y) { + int offset = y - mRv.getPaddingTop() - mThumbOffsetY; + + return x >= 0 && x < getWidth() && offset >= 0 && offset <= mThumbHeight; + } + + /** + * Returns true if AllAppsTransitionController can handle vertical motion + * beginning at this point. + */ + public boolean shouldBlockIntercept(int x, int y) { + return isNearThumb(x, y); + } + + /** + * Returns whether the specified x position is near the scroll bar. + */ + public boolean isNearScrollBar(int x) { + return x >= (getWidth() - mMaxWidth) / 2 && x <= (getWidth() + mMaxWidth) / 2; + } + + private void animatePopupVisibility(boolean visible) { + if (mPopupVisible != visible) { + mPopupVisible = visible; + mPopupView.animate().cancel(); + mPopupView.animate().alpha(visible ? 1f : 0f).setDuration(visible ? 200 : 150).start(); + } + } + + private void updatePopupY(int lastTouchY) { + int height = mPopupView.getHeight(); + float top = lastTouchY - (FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR * height) + + mRv.getPaddingTop(); + top = Utilities.boundToRange(top, + mMaxWidth, mRv.getScrollbarTrackHeight() - mMaxWidth - height); + mPopupView.setTranslationY(top); + } +} -- cgit v1.2.3