2015-06-16 13:35:04 -07:00
|
|
|
/*
|
|
|
|
|
* 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;
|
2015-08-20 12:23:52 -07:00
|
|
|
import android.graphics.Path;
|
2015-06-16 13:35:04 -07:00
|
|
|
import android.graphics.Rect;
|
2016-10-08 17:43:48 -07:00
|
|
|
import android.util.Property;
|
2015-06-16 13:35:04 -07:00
|
|
|
import android.view.MotionEvent;
|
2016-10-08 17:43:48 -07:00
|
|
|
import android.view.View;
|
2015-06-16 13:35:04 -07:00
|
|
|
import android.view.ViewConfiguration;
|
2016-10-08 17:43:48 -07:00
|
|
|
import android.widget.TextView;
|
2015-06-24 11:45:32 -07:00
|
|
|
|
2016-12-27 15:41:24 -08:00
|
|
|
import com.android.launcher3.config.FeatureFlags;
|
|
|
|
|
|
2015-06-16 13:35:04 -07:00
|
|
|
/**
|
|
|
|
|
* The track and scrollbar that shows when you scroll the list.
|
|
|
|
|
*/
|
|
|
|
|
public class BaseRecyclerViewFastScrollBar {
|
|
|
|
|
|
|
|
|
|
public interface FastScrollFocusableView {
|
2015-08-21 11:16:27 -07:00
|
|
|
void setFastScrollFocusState(final FastBitmapDrawable.State focusState, boolean animated);
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2015-06-16 13:35:04 -07:00
|
|
|
private final static int MAX_TRACK_ALPHA = 30;
|
|
|
|
|
private final static int SCROLL_BAR_VIS_DURATION = 150;
|
2016-10-08 17:43:48 -07:00
|
|
|
private static final float FAST_SCROLL_OVERLAY_Y_OFFSET_FACTOR = 1.5f;
|
|
|
|
|
|
|
|
|
|
private final Rect mTmpRect = new Rect();
|
|
|
|
|
private final BaseRecyclerView mRv;
|
|
|
|
|
|
|
|
|
|
private final boolean mIsRtl;
|
2015-06-16 13:35:04 -07:00
|
|
|
|
|
|
|
|
// The inset is the buffer around which a point will still register as a click on the scrollbar
|
2016-10-08 17:43:48 -07:00
|
|
|
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;
|
2015-06-16 13:35:04 -07:00
|
|
|
private boolean mIsDragging;
|
2015-08-18 17:43:02 -07:00
|
|
|
private boolean mIsThumbDetached;
|
|
|
|
|
private boolean mCanThumbDetach;
|
2015-08-27 10:19:48 -07:00
|
|
|
private boolean mIgnoreDragGesture;
|
2015-06-16 13:35:04 -07:00
|
|
|
|
|
|
|
|
// 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.
|
2016-10-08 17:43:48 -07:00
|
|
|
private int mTouchOffsetY;
|
|
|
|
|
private int mThumbOffsetY;
|
2015-06-16 13:35:04 -07:00
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
// Fast scroller popup
|
|
|
|
|
private TextView mPopupView;
|
|
|
|
|
private boolean mPopupVisible;
|
|
|
|
|
private String mPopupSectionName;
|
2015-06-16 13:35:04 -07:00
|
|
|
|
|
|
|
|
public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
|
|
|
|
|
mRv = rv;
|
|
|
|
|
mTrackPaint = new Paint();
|
|
|
|
|
mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
|
2015-08-20 12:23:52 -07:00
|
|
|
mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
|
2016-10-08 17:43:48 -07:00
|
|
|
|
2015-06-16 13:35:04 -07:00
|
|
|
mThumbPaint = new Paint();
|
2015-08-20 12:23:52 -07:00
|
|
|
mThumbPaint.setAntiAlias(true);
|
2016-10-08 17:43:48 -07:00
|
|
|
mThumbPaint.setColor(Utilities.getColorAccent(rv.getContext()));
|
2015-08-20 12:23:52 -07:00
|
|
|
mThumbPaint.setStyle(Paint.Style.FILL);
|
2016-10-08 17:43:48 -07:00
|
|
|
|
|
|
|
|
mWidth = mMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
|
|
|
|
|
mMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
|
2015-06-16 13:35:04 -07:00
|
|
|
mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
|
|
|
|
|
mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
|
2016-10-08 17:43:48 -07:00
|
|
|
mIsRtl = Utilities.isRtl(res);
|
|
|
|
|
updateThumbPath();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setPopupView(View popup) {
|
|
|
|
|
mPopupView = (TextView) popup;
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
|
2015-08-18 17:43:02 -07:00
|
|
|
public void setDetachThumbOnFastScroll() {
|
|
|
|
|
mCanThumbDetach = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void reattachThumbToScroll() {
|
|
|
|
|
mIsThumbDetached = false;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
private int getDrawLeft() {
|
|
|
|
|
return mIsRtl ? 0 : (mRv.getWidth() - mMaxWidth);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setThumbOffsetY(int y) {
|
|
|
|
|
if (mThumbOffsetY == y) {
|
2015-06-16 13:35:04 -07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
// 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);
|
2015-08-18 17:43:02 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
public int getThumbOffsetY() {
|
|
|
|
|
return mThumbOffsetY;
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
private void setTrackWidth(int width) {
|
|
|
|
|
if (mWidth == width) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
int left = getDrawLeft();
|
|
|
|
|
// Invalidate the whole scroll bar area.
|
2016-10-18 14:23:46 +01:00
|
|
|
mRv.invalidate(left, 0, left + mMaxWidth, mRv.getScrollbarTrackHeight());
|
2015-06-16 13:35:04 -07:00
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
mWidth = width;
|
2015-08-20 12:23:52 -07:00
|
|
|
updateThumbPath();
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
/**
|
|
|
|
|
* Updates the path for the thumb drawable.
|
|
|
|
|
*/
|
|
|
|
|
private void updateThumbPath() {
|
|
|
|
|
int smallWidth = mIsRtl ? mWidth : -mWidth;
|
|
|
|
|
int largeWidth = mIsRtl ? mMaxWidth : -mMaxWidth;
|
|
|
|
|
|
|
|
|
|
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();
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getThumbHeight() {
|
|
|
|
|
return mThumbHeight;
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-18 17:43:02 -07:00
|
|
|
public boolean isDraggingThumb() {
|
2015-06-16 13:35:04 -07:00
|
|
|
return mIsDragging;
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-18 17:43:02 -07:00
|
|
|
public boolean isThumbDetached() {
|
|
|
|
|
return mIsThumbDetached;
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-16 13:35:04 -07:00
|
|
|
/**
|
|
|
|
|
* 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:
|
2015-08-27 10:19:48 -07:00
|
|
|
if (isNearThumb(downX, downY)) {
|
2016-10-08 17:43:48 -07:00
|
|
|
mTouchOffsetY = downY - mThumbOffsetY;
|
2016-12-27 15:41:24 -08:00
|
|
|
} else if (FeatureFlags.LAUNCHER3_DIRECT_SCROLL
|
|
|
|
|
&& mRv.supportsFastScrolling()
|
|
|
|
|
&& isNearScrollBar(downX)) {
|
|
|
|
|
calcTouchOffsetAndPrepToFastScroll(downY, lastY);
|
|
|
|
|
updateFastScrollSectionNameAndThumbOffset(lastY, y);
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case MotionEvent.ACTION_MOVE:
|
2015-08-27 10:19:48 -07:00
|
|
|
// 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();
|
2015-09-03 11:46:11 -07:00
|
|
|
if (!mIsDragging && !mIgnoreDragGesture && mRv.supportsFastScrolling() &&
|
|
|
|
|
isNearThumb(downX, lastY) &&
|
2015-06-16 13:35:04 -07:00
|
|
|
Math.abs(y - downY) > config.getScaledTouchSlop()) {
|
2016-12-27 15:41:24 -08:00
|
|
|
calcTouchOffsetAndPrepToFastScroll(downY, lastY);
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
if (mIsDragging) {
|
2016-12-27 15:41:24 -08:00
|
|
|
updateFastScrollSectionNameAndThumbOffset(lastY, y);
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case MotionEvent.ACTION_UP:
|
|
|
|
|
case MotionEvent.ACTION_CANCEL:
|
2016-10-08 17:43:48 -07:00
|
|
|
mTouchOffsetY = 0;
|
2015-08-18 17:43:02 -07:00
|
|
|
mLastTouchY = 0;
|
2015-08-27 10:19:48 -07:00
|
|
|
mIgnoreDragGesture = false;
|
2015-06-24 16:59:31 -07:00
|
|
|
if (mIsDragging) {
|
|
|
|
|
mIsDragging = false;
|
2016-10-08 17:43:48 -07:00
|
|
|
animatePopupVisibility(false);
|
2015-08-21 11:16:27 -07:00
|
|
|
showActiveScrollbar(false);
|
2015-06-24 16:59:31 -07:00
|
|
|
}
|
2015-06-16 13:35:04 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-27 15:41:24 -08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-16 13:35:04 -07:00
|
|
|
public void draw(Canvas canvas) {
|
2016-10-08 17:43:48 -07:00
|
|
|
if (mThumbOffsetY < 0) {
|
2015-06-16 13:35:04 -07:00
|
|
|
return;
|
|
|
|
|
}
|
2016-10-08 17:43:48 -07:00
|
|
|
int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
|
|
|
|
|
if (!mIsRtl) {
|
|
|
|
|
canvas.translate(mRv.getWidth(), 0);
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
2016-10-08 17:43:48 -07:00
|
|
|
// Draw the track
|
|
|
|
|
int thumbWidth = mIsRtl ? mWidth : -mWidth;
|
2016-10-18 14:23:46 +01:00
|
|
|
canvas.drawRect(0, 0, thumbWidth, mRv.getScrollbarTrackHeight(), mTrackPaint);
|
2015-06-16 13:35:04 -07:00
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
canvas.translate(0, mThumbOffsetY);
|
|
|
|
|
canvas.drawPath(mThumbPath, mThumbPaint);
|
|
|
|
|
canvas.restoreToCount(saveCount);
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2016-10-08 17:43:48 -07:00
|
|
|
* Animates the width of the scrollbar.
|
2015-06-16 13:35:04 -07:00
|
|
|
*/
|
2015-08-21 11:16:27 -07:00
|
|
|
private void showActiveScrollbar(boolean isScrolling) {
|
2016-10-08 17:43:48 -07:00
|
|
|
if (mWidthAnimator != null) {
|
|
|
|
|
mWidthAnimator.cancel();
|
2015-08-20 12:23:52 -07:00
|
|
|
}
|
2015-06-16 13:35:04 -07:00
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
mWidthAnimator = ObjectAnimator.ofInt(this, TRACK_WIDTH,
|
|
|
|
|
isScrolling ? mMaxWidth : mMinWidth);
|
|
|
|
|
mWidthAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
|
|
|
|
|
mWidthAnimator.start();
|
2015-08-20 12:23:52 -07:00
|
|
|
}
|
|
|
|
|
|
2015-06-16 13:35:04 -07:00
|
|
|
/**
|
2016-12-27 15:41:24 -08:00
|
|
|
* Returns whether the specified point is inside the thumb bounds.
|
2015-06-16 13:35:04 -07:00
|
|
|
*/
|
2016-06-10 12:00:02 -07:00
|
|
|
public boolean isNearThumb(int x, int y) {
|
2016-10-08 17:43:48 -07:00
|
|
|
int left = getDrawLeft();
|
|
|
|
|
mTmpRect.set(left, mThumbOffsetY, left + mMaxWidth, mThumbOffsetY + mThumbHeight);
|
2015-06-16 13:35:04 -07:00
|
|
|
mTmpRect.inset(mTouchInset, mTouchInset);
|
|
|
|
|
return mTmpRect.contains(x, y);
|
|
|
|
|
}
|
2016-10-08 17:43:48 -07:00
|
|
|
|
2016-12-27 15:41:24 -08:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-08 17:43:48 -07:00
|
|
|
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);
|
2016-10-18 14:23:46 +01:00
|
|
|
top = Math.max(mMaxWidth, Math.min(top, mRv.getScrollbarTrackHeight() - mMaxWidth - height));
|
2016-10-08 17:43:48 -07:00
|
|
|
mPopupView.setTranslationY(top);
|
|
|
|
|
}
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|