mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-03-02 17:06:49 +00:00
Simplifying fast scroller logic
> Using a separate view for drawing the popup. This allows us to use elevation property instead of drawing the shadow as bitmap. > During the thumb animation, invalidating the full track width, instead of invalidating the track and thumb separately. > The thumb path is calculated at 0,0 and drawn using canvas.translate(). This avoids recalculating the path on every scroll. Change-Id: I48741e5b4432df0d939016db284d7aaf52cc2aa6
This commit is contained in:
@@ -15,21 +15,18 @@
|
||||
*/
|
||||
package com.android.launcher3;
|
||||
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ArgbEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.util.Property;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import com.android.launcher3.util.Thunk;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* The track and scrollbar that shows when you scroll the list.
|
||||
@@ -40,29 +37,46 @@ public class BaseRecyclerViewFastScrollBar {
|
||||
void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
|
||||
}
|
||||
|
||||
private static final Property<BaseRecyclerViewFastScrollBar, Integer> TRACK_WIDTH =
|
||||
new Property<BaseRecyclerViewFastScrollBar, Integer>(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 = 1.5f;
|
||||
|
||||
@Thunk BaseRecyclerView mRv;
|
||||
private BaseRecyclerViewFastScrollPopup mPopup;
|
||||
private final Rect mTmpRect = new Rect();
|
||||
private final BaseRecyclerView mRv;
|
||||
|
||||
private AnimatorSet mScrollbarAnimator;
|
||||
private final boolean mIsRtl;
|
||||
|
||||
private int mThumbInactiveColor;
|
||||
private int mThumbActiveColor;
|
||||
@Thunk Point mThumbOffset = new Point(-1, -1);
|
||||
@Thunk Paint mThumbPaint;
|
||||
private int mThumbMinWidth;
|
||||
private int mThumbMaxWidth;
|
||||
@Thunk int mThumbWidth;
|
||||
@Thunk int mThumbHeight;
|
||||
private int mThumbCurvature;
|
||||
private Path mThumbPath = new Path();
|
||||
private Paint mTrackPaint;
|
||||
private int mTrackWidth;
|
||||
private float mLastTouchY;
|
||||
// The inset is the buffer around which a point will still register as a click on the scrollbar
|
||||
private int mTouchInset;
|
||||
private final int mTouchInset;
|
||||
|
||||
private final int mMinWidth;
|
||||
private final int mMaxWidth;
|
||||
|
||||
// Current width of the track
|
||||
private int mWidth;
|
||||
private ObjectAnimator mWidthAnimator;
|
||||
|
||||
private final Path mThumbPath = new Path();
|
||||
private final Paint mThumbPaint;
|
||||
private final int mThumbHeight;
|
||||
|
||||
private final Paint mTrackPaint;
|
||||
|
||||
private float mLastTouchY;
|
||||
private boolean mIsDragging;
|
||||
private boolean mIsThumbDetached;
|
||||
private boolean mCanThumbDetach;
|
||||
@@ -70,27 +84,35 @@ public class BaseRecyclerViewFastScrollBar {
|
||||
|
||||
// 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 mTouchOffset;
|
||||
private int mTouchOffsetY;
|
||||
private int mThumbOffsetY;
|
||||
|
||||
private Rect mInvalidateRect = new Rect();
|
||||
private Rect mTmpRect = new Rect();
|
||||
// Fast scroller popup
|
||||
private TextView mPopupView;
|
||||
private boolean mPopupVisible;
|
||||
private String mPopupSectionName;
|
||||
|
||||
public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
|
||||
mRv = rv;
|
||||
mPopup = new BaseRecyclerViewFastScrollPopup(rv, res);
|
||||
mTrackPaint = new Paint();
|
||||
mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
|
||||
mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
|
||||
mThumbActiveColor = mThumbInactiveColor = Utilities.getColorAccent(rv.getContext());
|
||||
|
||||
mThumbPaint = new Paint();
|
||||
mThumbPaint.setAntiAlias(true);
|
||||
mThumbPaint.setColor(mThumbInactiveColor);
|
||||
mThumbPaint.setColor(Utilities.getColorAccent(rv.getContext()));
|
||||
mThumbPaint.setStyle(Paint.Style.FILL);
|
||||
mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
|
||||
mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
|
||||
|
||||
mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
|
||||
mMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
|
||||
mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
|
||||
mThumbCurvature = mThumbMaxWidth - mThumbMinWidth;
|
||||
mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
|
||||
mIsRtl = Utilities.isRtl(res);
|
||||
updateThumbPath();
|
||||
}
|
||||
|
||||
public void setPopupView(View popup) {
|
||||
mPopupView = (TextView) popup;
|
||||
}
|
||||
|
||||
public void setDetachThumbOnFastScroll() {
|
||||
@@ -101,51 +123,54 @@ public class BaseRecyclerViewFastScrollBar {
|
||||
mIsThumbDetached = false;
|
||||
}
|
||||
|
||||
public void setThumbOffset(int x, int y) {
|
||||
if (mThumbOffset.x == x && mThumbOffset.y == y) {
|
||||
private int getDrawLeft() {
|
||||
return mIsRtl ? 0 : (mRv.getWidth() - mMaxWidth);
|
||||
}
|
||||
|
||||
public void setThumbOffsetY(int y) {
|
||||
if (mThumbOffsetY == y) {
|
||||
return;
|
||||
}
|
||||
mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
|
||||
mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
|
||||
mThumbOffset.set(x, y);
|
||||
|
||||
// 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);
|
||||
mRv.invalidate(mTmpRect);
|
||||
}
|
||||
|
||||
public int getThumbOffsetY() {
|
||||
return mThumbOffsetY;
|
||||
}
|
||||
|
||||
private void setTrackWidth(int width) {
|
||||
if (mWidth == width) {
|
||||
return;
|
||||
}
|
||||
int left = getDrawLeft();
|
||||
// Invalidate the whole scroll bar area.
|
||||
mRv.invalidate(left, 0, left + mMaxWidth, mRv.getVisibleHeight());
|
||||
|
||||
mWidth = width;
|
||||
updateThumbPath();
|
||||
mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
|
||||
mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
|
||||
mRv.invalidate(mInvalidateRect);
|
||||
}
|
||||
|
||||
public Point getThumbOffset() {
|
||||
return mThumbOffset;
|
||||
}
|
||||
/**
|
||||
* Updates the path for the thumb drawable.
|
||||
*/
|
||||
private void updateThumbPath() {
|
||||
int smallWidth = mIsRtl ? mWidth : -mWidth;
|
||||
int largeWidth = mIsRtl ? mMaxWidth : -mMaxWidth;
|
||||
|
||||
// Setter/getter for the thumb bar width for animations
|
||||
public void setThumbWidth(int width) {
|
||||
mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
|
||||
mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
|
||||
mThumbWidth = width;
|
||||
updateThumbPath();
|
||||
mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
|
||||
mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
|
||||
mRv.invalidate(mInvalidateRect);
|
||||
}
|
||||
|
||||
public int getThumbWidth() {
|
||||
return mThumbWidth;
|
||||
}
|
||||
|
||||
// Setter/getter for the track bar width for animations
|
||||
public void setTrackWidth(int width) {
|
||||
mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
|
||||
mRv.getVisibleHeight());
|
||||
mTrackWidth = width;
|
||||
updateThumbPath();
|
||||
mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
|
||||
mRv.getVisibleHeight());
|
||||
mRv.invalidate(mInvalidateRect);
|
||||
}
|
||||
|
||||
public int getTrackWidth() {
|
||||
return mTrackWidth;
|
||||
mThumbPath.reset();
|
||||
mThumbPath.moveTo(0, 0);
|
||||
mThumbPath.lineTo(0, mThumbHeight); // Left edge
|
||||
mThumbPath.lineTo(smallWidth, mThumbHeight); // bottom edge
|
||||
mThumbPath.cubicTo(smallWidth, mThumbHeight, // right edge
|
||||
largeWidth, mThumbHeight / 2,
|
||||
smallWidth, 0);
|
||||
mThumbPath.close();
|
||||
}
|
||||
|
||||
public int getThumbHeight() {
|
||||
@@ -153,7 +178,7 @@ public class BaseRecyclerViewFastScrollBar {
|
||||
}
|
||||
|
||||
public int getThumbMaxWidth() {
|
||||
return mThumbMaxWidth;
|
||||
return mMaxWidth;
|
||||
}
|
||||
|
||||
public boolean isDraggingThumb() {
|
||||
@@ -176,7 +201,7 @@ public class BaseRecyclerViewFastScrollBar {
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (isNearThumb(downX, downY)) {
|
||||
mTouchOffset = downY - mThumbOffset.y;
|
||||
mTouchOffsetY = downY - mThumbOffsetY;
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
@@ -191,32 +216,35 @@ public class BaseRecyclerViewFastScrollBar {
|
||||
if (mCanThumbDetach) {
|
||||
mIsThumbDetached = true;
|
||||
}
|
||||
mTouchOffset += (lastY - downY);
|
||||
mPopup.animateVisibility(true);
|
||||
mTouchOffsetY += (lastY - downY);
|
||||
animatePopupVisibility(true);
|
||||
showActiveScrollbar(true);
|
||||
}
|
||||
if (mIsDragging) {
|
||||
// Update the fastscroller section name at this touch position
|
||||
int top = mRv.getBackgroundPadding().top;
|
||||
int bottom = top + mRv.getVisibleHeight() - mThumbHeight;
|
||||
float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset));
|
||||
float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffsetY));
|
||||
String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) /
|
||||
(bottom - top));
|
||||
mPopup.setSectionName(sectionName);
|
||||
mPopup.animateVisibility(!sectionName.isEmpty());
|
||||
mRv.invalidate(mPopup.updateFastScrollerBounds(lastY));
|
||||
if (!sectionName.equals(mPopupSectionName)) {
|
||||
mPopupSectionName = sectionName;
|
||||
mPopupView.setText(sectionName);
|
||||
}
|
||||
animatePopupVisibility(!sectionName.isEmpty());
|
||||
updatePopupY(lastY);
|
||||
mLastTouchY = boundedY;
|
||||
setThumbOffset(mRv.getScrollBarX(), (int) mLastTouchY);
|
||||
setThumbOffsetY((int) mLastTouchY);
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
mTouchOffset = 0;
|
||||
mTouchOffsetY = 0;
|
||||
mLastTouchY = 0;
|
||||
mIgnoreDragGesture = false;
|
||||
if (mIsDragging) {
|
||||
mIsDragging = false;
|
||||
mPopup.animateVisibility(false);
|
||||
animatePopupVisibility(false);
|
||||
showActiveScrollbar(false);
|
||||
}
|
||||
break;
|
||||
@@ -224,74 +252,58 @@ public class BaseRecyclerViewFastScrollBar {
|
||||
}
|
||||
|
||||
public void draw(Canvas canvas) {
|
||||
if (mThumbOffset.x < 0 || mThumbOffset.y < 0) {
|
||||
if (mThumbOffsetY < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw the scroll bar track and thumb
|
||||
if (mTrackPaint.getAlpha() > 0) {
|
||||
canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth,
|
||||
mRv.getVisibleHeight(), mTrackPaint);
|
||||
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
|
||||
if (!mIsRtl) {
|
||||
canvas.translate(mRv.getWidth(), 0);
|
||||
}
|
||||
canvas.drawPath(mThumbPath, mThumbPaint);
|
||||
// Draw the track
|
||||
int thumbWidth = mIsRtl ? mWidth : -mWidth;
|
||||
canvas.drawRect(0, 0, thumbWidth, mRv.getVisibleHeight(), mTrackPaint);
|
||||
|
||||
// Draw the popup
|
||||
mPopup.draw(canvas);
|
||||
canvas.translate(0, mThumbOffsetY);
|
||||
canvas.drawPath(mThumbPath, mThumbPaint);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates the width and color of the scrollbar.
|
||||
* Animates the width of the scrollbar.
|
||||
*/
|
||||
private void showActiveScrollbar(boolean isScrolling) {
|
||||
if (mScrollbarAnimator != null) {
|
||||
mScrollbarAnimator.cancel();
|
||||
if (mWidthAnimator != null) {
|
||||
mWidthAnimator.cancel();
|
||||
}
|
||||
|
||||
mScrollbarAnimator = new AnimatorSet();
|
||||
ObjectAnimator trackWidthAnim = ObjectAnimator.ofInt(this, "trackWidth",
|
||||
isScrolling ? mThumbMaxWidth : mThumbMinWidth);
|
||||
ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
|
||||
isScrolling ? mThumbMaxWidth : mThumbMinWidth);
|
||||
mScrollbarAnimator.playTogether(trackWidthAnim, thumbWidthAnim);
|
||||
if (mThumbActiveColor != mThumbInactiveColor) {
|
||||
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(),
|
||||
mThumbPaint.getColor(), isScrolling ? mThumbActiveColor : mThumbInactiveColor);
|
||||
colorAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animator) {
|
||||
mThumbPaint.setColor((Integer) animator.getAnimatedValue());
|
||||
mRv.invalidate(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
|
||||
mThumbOffset.y + mThumbHeight);
|
||||
}
|
||||
});
|
||||
mScrollbarAnimator.play(colorAnimation);
|
||||
}
|
||||
mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
|
||||
mScrollbarAnimator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the path for the thumb drawable.
|
||||
*/
|
||||
private void updateThumbPath() {
|
||||
mThumbCurvature = mThumbMaxWidth - mThumbWidth;
|
||||
mThumbPath.reset();
|
||||
mThumbPath.moveTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y); // tr
|
||||
mThumbPath.lineTo(mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight); // br
|
||||
mThumbPath.lineTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight); // bl
|
||||
mThumbPath.cubicTo(mThumbOffset.x, mThumbOffset.y + mThumbHeight,
|
||||
mThumbOffset.x - mThumbCurvature, mThumbOffset.y + mThumbHeight / 2,
|
||||
mThumbOffset.x, mThumbOffset.y); // bl2tl
|
||||
mThumbPath.close();
|
||||
mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH,
|
||||
isScrolling ? mMaxWidth : mMinWidth);
|
||||
mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
|
||||
mWidthAnimator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the specified points are near the scroll bar bounds.
|
||||
*/
|
||||
public boolean isNearThumb(int x, int y) {
|
||||
mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
|
||||
mThumbOffset.y + mThumbHeight);
|
||||
int left = getDrawLeft();
|
||||
mTmpRect.set(left, mThumbOffsetY, left + mMaxWidth, mThumbOffsetY + mThumbHeight);
|
||||
mTmpRect.inset(mTouchInset, mTouchInset);
|
||||
return mTmpRect.contains(x, y);
|
||||
}
|
||||
|
||||
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 = Math.max(mMaxWidth, Math.min(top, mRv.getVisibleHeight() - mMaxWidth - height));
|
||||
mPopupView.setTranslationY(top);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user