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.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;
|
2015-08-20 12:23:52 -07:00
|
|
|
import android.graphics.Path;
|
2015-06-16 13:35:04 -07:00
|
|
|
import android.graphics.Point;
|
|
|
|
|
import android.graphics.Rect;
|
|
|
|
|
import android.view.MotionEvent;
|
|
|
|
|
import android.view.ViewConfiguration;
|
|
|
|
|
|
2015-06-24 11:45:32 -07:00
|
|
|
import com.android.launcher3.util.Thunk;
|
|
|
|
|
|
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 {
|
|
|
|
|
void setFastScrollFocused(boolean focused, boolean animated);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private final static int MAX_TRACK_ALPHA = 30;
|
|
|
|
|
private final static int SCROLL_BAR_VIS_DURATION = 150;
|
|
|
|
|
|
2015-06-24 11:45:32 -07:00
|
|
|
@Thunk BaseRecyclerView mRv;
|
2015-06-16 13:35:04 -07:00
|
|
|
private BaseRecyclerViewFastScrollPopup mPopup;
|
|
|
|
|
|
|
|
|
|
private AnimatorSet mScrollbarAnimator;
|
|
|
|
|
|
|
|
|
|
private int mThumbInactiveColor;
|
|
|
|
|
private int mThumbActiveColor;
|
2015-06-24 11:45:32 -07:00
|
|
|
@Thunk Point mThumbOffset = new Point(-1, -1);
|
|
|
|
|
@Thunk Paint mThumbPaint;
|
2015-06-16 13:35:04 -07:00
|
|
|
private int mThumbMinWidth;
|
|
|
|
|
private int mThumbMaxWidth;
|
2015-06-24 11:45:32 -07:00
|
|
|
@Thunk int mThumbWidth;
|
|
|
|
|
@Thunk int mThumbHeight;
|
2015-08-20 12:23:52 -07:00
|
|
|
private int mThumbCurvature;
|
|
|
|
|
private Path mThumbPath = new Path();
|
|
|
|
|
private Paint mTrackPaint;
|
|
|
|
|
private int mTrackWidth;
|
2015-08-18 17:43:02 -07:00
|
|
|
private float mLastTouchY;
|
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
|
|
|
|
|
private int mTouchInset;
|
|
|
|
|
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.
|
|
|
|
|
private int mTouchOffset;
|
|
|
|
|
|
|
|
|
|
private Rect mInvalidateRect = new Rect();
|
|
|
|
|
private Rect mTmpRect = new Rect();
|
|
|
|
|
|
|
|
|
|
public BaseRecyclerViewFastScrollBar(BaseRecyclerView rv, Resources res) {
|
|
|
|
|
mRv = rv;
|
|
|
|
|
mPopup = new BaseRecyclerViewFastScrollPopup(rv, res);
|
|
|
|
|
mTrackPaint = new Paint();
|
|
|
|
|
mTrackPaint.setColor(rv.getFastScrollerTrackColor(Color.BLACK));
|
2015-08-20 12:23:52 -07:00
|
|
|
mTrackPaint.setAlpha(MAX_TRACK_ALPHA);
|
2015-06-16 13:35:04 -07:00
|
|
|
mThumbInactiveColor = rv.getFastScrollerThumbInactiveColor(
|
|
|
|
|
res.getColor(R.color.container_fastscroll_thumb_inactive_color));
|
|
|
|
|
mThumbActiveColor = res.getColor(R.color.container_fastscroll_thumb_active_color);
|
|
|
|
|
mThumbPaint = new Paint();
|
2015-08-20 12:23:52 -07:00
|
|
|
mThumbPaint.setAntiAlias(true);
|
2015-06-16 13:35:04 -07:00
|
|
|
mThumbPaint.setColor(mThumbInactiveColor);
|
2015-08-20 12:23:52 -07:00
|
|
|
mThumbPaint.setStyle(Paint.Style.FILL);
|
2015-06-16 13:35:04 -07:00
|
|
|
mThumbWidth = mThumbMinWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_min_width);
|
|
|
|
|
mThumbMaxWidth = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_max_width);
|
|
|
|
|
mThumbHeight = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_height);
|
2015-08-20 12:23:52 -07:00
|
|
|
mThumbCurvature = mThumbMaxWidth - mThumbMinWidth;
|
2015-06-16 13:35:04 -07:00
|
|
|
mTouchInset = res.getDimensionPixelSize(R.dimen.container_fastscroll_thumb_touch_inset);
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-18 17:43:02 -07:00
|
|
|
public void setDetachThumbOnFastScroll() {
|
|
|
|
|
mCanThumbDetach = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void reattachThumbToScroll() {
|
|
|
|
|
mIsThumbDetached = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setThumbOffset(int x, int y) {
|
2015-06-16 13:35:04 -07:00
|
|
|
if (mThumbOffset.x == x && mThumbOffset.y == y) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2015-08-20 12:23:52 -07:00
|
|
|
mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
|
|
|
|
|
mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
|
2015-06-16 13:35:04 -07:00
|
|
|
mThumbOffset.set(x, y);
|
2015-08-20 12:23:52 -07:00
|
|
|
updateThumbPath();
|
|
|
|
|
mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
|
|
|
|
|
mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
|
2015-06-16 13:35:04 -07:00
|
|
|
mRv.invalidate(mInvalidateRect);
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-18 17:43:02 -07:00
|
|
|
public Point getThumbOffset() {
|
|
|
|
|
return mThumbOffset;
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-20 12:23:52 -07:00
|
|
|
// Setter/getter for the thumb bar width for animations
|
2015-08-18 17:43:02 -07:00
|
|
|
public void setThumbWidth(int width) {
|
2015-08-20 12:23:52 -07:00
|
|
|
mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
|
|
|
|
|
mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
|
2015-06-16 13:35:04 -07:00
|
|
|
mThumbWidth = width;
|
2015-08-20 12:23:52 -07:00
|
|
|
updateThumbPath();
|
|
|
|
|
mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, mThumbOffset.y,
|
|
|
|
|
mThumbOffset.x + mThumbWidth, mThumbOffset.y + mThumbHeight);
|
2015-06-16 13:35:04 -07:00
|
|
|
mRv.invalidate(mInvalidateRect);
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-18 17:43:02 -07:00
|
|
|
public int getThumbWidth() {
|
2015-06-16 13:35:04 -07:00
|
|
|
return mThumbWidth;
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-20 12:23:52 -07:00
|
|
|
// Setter/getter for the track bar width for animations
|
|
|
|
|
public void setTrackWidth(int width) {
|
|
|
|
|
mInvalidateRect.set(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
|
|
|
|
|
mRv.getHeight());
|
|
|
|
|
mTrackWidth = width;
|
|
|
|
|
updateThumbPath();
|
|
|
|
|
mInvalidateRect.union(mThumbOffset.x - mThumbCurvature, 0, mThumbOffset.x + mThumbWidth,
|
|
|
|
|
mRv.getHeight());
|
2015-06-16 13:35:04 -07:00
|
|
|
mRv.invalidate(mInvalidateRect);
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-20 12:23:52 -07:00
|
|
|
public int getTrackWidth() {
|
|
|
|
|
return mTrackWidth;
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getThumbHeight() {
|
|
|
|
|
return mThumbHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getThumbMaxWidth() {
|
|
|
|
|
return mThumbMaxWidth;
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-18 17:43:02 -07:00
|
|
|
public float getLastTouchY() {
|
|
|
|
|
return mLastTouchY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)) {
|
2015-06-16 13:35:04 -07:00
|
|
|
mTouchOffset = downY - mThumbOffset.y;
|
|
|
|
|
}
|
|
|
|
|
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();
|
|
|
|
|
if (!mIsDragging && !mIgnoreDragGesture && isNearThumb(downX, lastY) &&
|
2015-06-16 13:35:04 -07:00
|
|
|
Math.abs(y - downY) > config.getScaledTouchSlop()) {
|
|
|
|
|
mRv.getParent().requestDisallowInterceptTouchEvent(true);
|
|
|
|
|
mIsDragging = true;
|
2015-08-18 17:43:02 -07:00
|
|
|
if (mCanThumbDetach) {
|
|
|
|
|
mIsThumbDetached = true;
|
|
|
|
|
}
|
2015-06-16 13:35:04 -07:00
|
|
|
mTouchOffset += (lastY - downY);
|
|
|
|
|
mPopup.animateVisibility(true);
|
|
|
|
|
animateScrollbar(true);
|
|
|
|
|
}
|
|
|
|
|
if (mIsDragging) {
|
|
|
|
|
// Update the fastscroller section name at this touch position
|
|
|
|
|
int top = mRv.getBackgroundPadding().top;
|
|
|
|
|
int bottom = mRv.getHeight() - mRv.getBackgroundPadding().bottom - mThumbHeight;
|
|
|
|
|
float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset));
|
|
|
|
|
String sectionName = mRv.scrollToPositionAtProgress((boundedY - top) /
|
|
|
|
|
(bottom - top));
|
|
|
|
|
mPopup.setSectionName(sectionName);
|
|
|
|
|
mPopup.animateVisibility(!sectionName.isEmpty());
|
|
|
|
|
mRv.invalidate(mPopup.updateFastScrollerBounds(mRv, lastY));
|
2015-08-18 17:43:02 -07:00
|
|
|
mLastTouchY = boundedY;
|
2015-06-16 13:35:04 -07:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case MotionEvent.ACTION_UP:
|
|
|
|
|
case MotionEvent.ACTION_CANCEL:
|
|
|
|
|
mTouchOffset = 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;
|
|
|
|
|
mPopup.animateVisibility(false);
|
|
|
|
|
animateScrollbar(false);
|
|
|
|
|
}
|
2015-06-16 13:35:04 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void draw(Canvas canvas) {
|
|
|
|
|
if (mThumbOffset.x < 0 || mThumbOffset.y < 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw the scroll bar track and thumb
|
|
|
|
|
if (mTrackPaint.getAlpha() > 0) {
|
|
|
|
|
canvas.drawRect(mThumbOffset.x, 0, mThumbOffset.x + mThumbWidth, mRv.getHeight(), mTrackPaint);
|
|
|
|
|
}
|
2015-08-20 12:23:52 -07:00
|
|
|
canvas.drawPath(mThumbPath, mThumbPaint);
|
2015-06-16 13:35:04 -07:00
|
|
|
|
|
|
|
|
// Draw the popup
|
|
|
|
|
mPopup.draw(canvas);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Animates the width and color of the scrollbar.
|
|
|
|
|
*/
|
|
|
|
|
private void animateScrollbar(boolean isScrolling) {
|
|
|
|
|
if (mScrollbarAnimator != null) {
|
|
|
|
|
mScrollbarAnimator.cancel();
|
|
|
|
|
}
|
2015-08-20 12:23:52 -07:00
|
|
|
|
|
|
|
|
mScrollbarAnimator = new AnimatorSet();
|
|
|
|
|
ObjectAnimator trackWidthAnim = ObjectAnimator.ofInt(this, "trackWidth",
|
|
|
|
|
isScrolling ? mThumbMaxWidth : mThumbMinWidth);
|
2015-08-18 17:43:02 -07:00
|
|
|
ObjectAnimator thumbWidthAnim = ObjectAnimator.ofInt(this, "thumbWidth",
|
2015-06-16 13:35:04 -07:00
|
|
|
isScrolling ? mThumbMaxWidth : mThumbMinWidth);
|
2015-08-20 12:23:52 -07:00
|
|
|
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);
|
|
|
|
|
}
|
2015-06-16 13:35:04 -07:00
|
|
|
mScrollbarAnimator.setDuration(SCROLL_BAR_VIS_DURATION);
|
|
|
|
|
mScrollbarAnimator.start();
|
|
|
|
|
}
|
|
|
|
|
|
2015-08-20 12:23:52 -07:00
|
|
|
/**
|
|
|
|
|
* 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();
|
|
|
|
|
}
|
|
|
|
|
|
2015-06-16 13:35:04 -07:00
|
|
|
/**
|
|
|
|
|
* Returns whether the specified points are near the scroll bar bounds.
|
|
|
|
|
*/
|
2015-08-27 10:19:48 -07:00
|
|
|
private boolean isNearThumb(int x, int y) {
|
2015-06-16 13:35:04 -07:00
|
|
|
mTmpRect.set(mThumbOffset.x, mThumbOffset.y, mThumbOffset.x + mThumbWidth,
|
|
|
|
|
mThumbOffset.y + mThumbHeight);
|
|
|
|
|
mTmpRect.inset(mTouchInset, mTouchInset);
|
|
|
|
|
return mTmpRect.contains(x, y);
|
|
|
|
|
}
|
|
|
|
|
}
|