From b7e4e09edd463fd5efec9ed6fcd9dad356e109cd Mon Sep 17 00:00:00 2001 From: cgspine Date: Thu, 30 May 2019 16:47:04 +0800 Subject: [PATCH] draggable scroll bar --- ...MUIContinuousNestedBottomRecyclerView.java | 1 + .../QMUIContinuousNestedScrollLayout.java | 77 +++++++- .../QMUIContinuousNestedTopRecyclerView.java | 7 + .../QMUIContinuousNestedTopWebView.java | 7 + .../nestedScroll/QMUIDraggableScrollBar.java | 187 ++++++++++++++++++ .../drawable-xhdpi/qmui_icon_scroll_bar.png | Bin 0 -> 2817 bytes .../drawable-xxhdpi/qmui_icon_scroll_bar.png | Bin 0 -> 5277 bytes .../drawable-xxxhdpi/qmui_icon_scroll_bar.png | Bin 0 -> 8503 bytes 8 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIDraggableScrollBar.java create mode 100644 qmui/src/main/res/drawable-xhdpi/qmui_icon_scroll_bar.png create mode 100644 qmui/src/main/res/drawable-xxhdpi/qmui_icon_scroll_bar.png create mode 100644 qmui/src/main/res/drawable-xxxhdpi/qmui_icon_scroll_bar.png diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomRecyclerView.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomRecyclerView.java index ce0be273..aa71f429 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomRecyclerView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedBottomRecyclerView.java @@ -49,6 +49,7 @@ public class QMUIContinuousNestedBottomRecyclerView extends RecyclerView } private void init() { + setVerticalScrollBarEnabled(false); addOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedScrollLayout.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedScrollLayout.java index d49c3f80..922ceee1 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedScrollLayout.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedScrollLayout.java @@ -18,6 +18,7 @@ package com.qmuiteam.qmui.nestedScroll; import android.content.Context; import android.util.AttributeSet; +import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -32,7 +33,7 @@ import androidx.annotation.Nullable; import androidx.coordinatorlayout.widget.CoordinatorLayout; public class QMUIContinuousNestedScrollLayout extends CoordinatorLayout implements - QMUIContinuousNestedTopAreaBehavior.Callback { + QMUIContinuousNestedTopAreaBehavior.Callback, QMUIDraggableScrollBar.Callback { private IQMUIContinuousNestedTopView mTopView; private IQMUIContinuousNestedBottomView mBottomView; @@ -47,19 +48,80 @@ public class QMUIContinuousNestedScrollLayout extends CoordinatorLayout implemen } }; private boolean mKeepBottomAreaStableWhenCheckLayout = false; + private QMUIDraggableScrollBar mDraggableScrollBar; + private boolean mIsDraggableScrollBarEnabled = false; public QMUIContinuousNestedScrollLayout(@NonNull Context context) { - super(context); + this(context, null); } public QMUIContinuousNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); } public QMUIContinuousNestedScrollLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } + private void ensureScrollBar(){ + if(mDraggableScrollBar == null){ + mDraggableScrollBar = createScrollBar(getContext()); + mDraggableScrollBar.setCallback(this); + CoordinatorLayout.LayoutParams lp = new CoordinatorLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); + lp.gravity = Gravity.RIGHT; + addView(mDraggableScrollBar, lp); + } + } + + public void setDraggableScrollBarEnabled(boolean draggableScrollBarEnabled) { + mIsDraggableScrollBarEnabled = draggableScrollBarEnabled; + } + + protected QMUIDraggableScrollBar createScrollBar(Context context){ + return new QMUIDraggableScrollBar(context); + } + + @Override + public void onDragToPercent(float percent) { + int targetScroll = (int) (getScrollRange() * percent); + scrollBy(targetScroll - getCurrentScroll()); + } + + public int getCurrentScroll() { + int currentScroll = 0; + if (mTopView != null) { + currentScroll += mTopView.getCurrentScroll(); + } + currentScroll += getOffsetCurrent(); + if (mBottomView != null) { + currentScroll += mBottomView.getCurrentScroll(); + } + return currentScroll; + } + + public int getScrollRange() { + int totalRange = 0; + if (mTopView != null) { + totalRange += mTopView.getScrollOffsetRange(); + } + totalRange += getOffsetRange(); + + if (mBottomView != null) { + totalRange += mBottomView.getScrollOffsetRange(); + } + return totalRange; + } + + public float getCurrentScrollPercent() { + int scrollRange = getScrollRange(); + if (scrollRange == 0) { + return 0; + } + return getCurrentScroll() * 1f / scrollRange; + } + + public void addOnScrollListener(@NonNull OnScrollListener onScrollListener) { if (!mOnScrollListeners.contains(onScrollListener)) { mOnScrollListeners.add(onScrollListener); @@ -112,7 +174,7 @@ public class QMUIContinuousNestedScrollLayout extends CoordinatorLayout implemen layoutParams.setBehavior(mTopAreaBehavior); } mTopAreaBehavior.setCallback(this); - addView(topView, layoutParams); + addView(topView, 0, layoutParams); } public IQMUIContinuousNestedTopView getTopView() { @@ -164,7 +226,7 @@ public class QMUIContinuousNestedScrollLayout extends CoordinatorLayout implemen mBottomAreaBehavior = new QMUIContinuousNestedBottomAreaBehavior(); layoutParams.setBehavior(mBottomAreaBehavior); } - addView(bottomView, layoutParams); + addView(bottomView, 0, layoutParams); } @Override @@ -229,6 +291,11 @@ public class QMUIContinuousNestedScrollLayout extends CoordinatorLayout implemen private void dispatchScroll(int topCurrent, int topRange, int offsetCurrent, int offsetRange, int bottomCurrent, int bottomRange) { + if(mIsDraggableScrollBarEnabled){ + ensureScrollBar(); + mDraggableScrollBar.setPercent(getCurrentScrollPercent()); + mDraggableScrollBar.awakenScrollBar(); + } for (OnScrollListener onScrollListener : mOnScrollListeners) { onScrollListener.onScroll(topCurrent, topRange, offsetCurrent, offsetRange, bottomCurrent, bottomRange); diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopRecyclerView.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopRecyclerView.java index 1f1a8d7e..07f99438 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopRecyclerView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopRecyclerView.java @@ -33,14 +33,21 @@ public class QMUIContinuousNestedTopRecyclerView extends RecyclerView implements public QMUIContinuousNestedTopRecyclerView(@NonNull Context context) { this(context, null); + init(); } public QMUIContinuousNestedTopRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); + init(); } public QMUIContinuousNestedTopRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + init(); + } + + private void init(){ + setVerticalScrollBarEnabled(false); } @Override diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopWebView.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopWebView.java index e39755d5..84d85ed6 100644 --- a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopWebView.java +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIContinuousNestedTopWebView.java @@ -29,14 +29,21 @@ public class QMUIContinuousNestedTopWebView extends QMUIWebView implements IQMUI public QMUIContinuousNestedTopWebView(Context context) { super(context); + init(); } public QMUIContinuousNestedTopWebView(Context context, AttributeSet attrs) { super(context, attrs); + init(); } public QMUIContinuousNestedTopWebView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + init(); + } + + private void init(){ + setVerticalScrollBarEnabled(false); } @Override diff --git a/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIDraggableScrollBar.java b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIDraggableScrollBar.java new file mode 100644 index 00000000..e1003c34 --- /dev/null +++ b/qmui/src/main/java/com/qmuiteam/qmui/nestedScroll/QMUIDraggableScrollBar.java @@ -0,0 +1,187 @@ +package com.qmuiteam.qmui.nestedScroll; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; + +import com.qmuiteam.qmui.R; +import com.qmuiteam.qmui.util.QMUILangHelper; + +public class QMUIDraggableScrollBar extends View { + + private Drawable mDragDrawable; + private int mKeepShownTime = 800; + private int mTransitionDuration = 100; + private long mStartTransitionTime = 0; + private float mCurrentAlpha = 0f; + private float mPercent = 0f; + private Runnable mDelayInvalidateRunnable = new Runnable() { + @Override + public void run() { + invalidate(); + } + }; + private boolean mIsInDragging = false; + private Callback mCallback; + + public QMUIDraggableScrollBar(Context context) { + this(context, (AttributeSet) null); + } + + public QMUIDraggableScrollBar(Context context, @NonNull Drawable dragDrawable) { + this(context, (AttributeSet) null); + mDragDrawable = dragDrawable.mutate(); + } + + public QMUIDraggableScrollBar(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public void setKeepShownTime(int keepShownTime) { + mKeepShownTime = keepShownTime; + } + + public void setTransitionDuration(int transitionDuration) { + mTransitionDuration = transitionDuration; + } + + public void setDragDrawable(Drawable dragDrawable) { + mDragDrawable = dragDrawable.mutate(); + invalidate(); + } + + public void setPercent(float percent) { + mPercent = percent; + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + Drawable drawable = mDragDrawable; + if (drawable == null) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + return; + } + super.onMeasure(MeasureSpec.makeMeasureSpec( + drawable.getIntrinsicWidth(), MeasureSpec.EXACTLY), heightMeasureSpec); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + Drawable drawable = mDragDrawable; + if (drawable == null) { + return super.onTouchEvent(event); + } + int action = event.getAction(); + final float x = event.getX(); + final float y = event.getY(); + if (action == MotionEvent.ACTION_DOWN) { + mIsInDragging = false; + float drawableY = mPercent * (getHeight() - drawable.getIntrinsicHeight()); + if (mCurrentAlpha > 0 && x > getWidth() - drawable.getIntrinsicWidth() + && y >= drawableY && y <= drawableY + drawable.getIntrinsicHeight()) { + getParent().requestDisallowInterceptTouchEvent(true); + mIsInDragging = true; + } + } else if (action == MotionEvent.ACTION_MOVE) { + if (mIsInDragging) { + getParent().requestDisallowInterceptTouchEvent(true); + onDragging(drawable, y); + } + } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + if (mIsInDragging) { + mIsInDragging = false; + onDragging(drawable, y); + } + } + return mIsInDragging; + } + + private void onDragging(Drawable drawable, float currentY) { + float percent = currentY / (getHeight() - drawable.getIntrinsicHeight()); + percent = QMUILangHelper.constrain(percent, 0f, 1f); + if (mCallback != null) { + mCallback.onDragToPercent(percent); + } + setPercent(percent); + } + + public void awakenScrollBar() { + if (mDragDrawable == null) { + mDragDrawable = ContextCompat.getDrawable(getContext(), R.drawable.qmui_icon_scroll_bar); + } + long current = System.currentTimeMillis(); + if (current - mStartTransitionTime > mTransitionDuration) { + mStartTransitionTime = current - mTransitionDuration; + } + ViewCompat.postInvalidateOnAnimation(this); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + Drawable drawable = mDragDrawable; + if (drawable == null) { + return; + } + int drawableWidth = drawable.getIntrinsicWidth(); + int drawableHeight = drawable.getIntrinsicHeight(); + if (drawableWidth <= 0 || drawableHeight <= 0) { + return; + } + long timeAfterStartShow = System.currentTimeMillis() - mStartTransitionTime; + Log.i("cgine", "timeAfterStartShow = " + timeAfterStartShow); + long timeAfterEndShow; + int needInvalidate = -1; + if (timeAfterStartShow < mTransitionDuration) { + // in show animation + mCurrentAlpha = timeAfterStartShow * 1f / mTransitionDuration; + needInvalidate = 0; + } else if (timeAfterStartShow - mTransitionDuration < mKeepShownTime) { + // keep show + mCurrentAlpha = 1f; + needInvalidate = (int) (mKeepShownTime - (timeAfterStartShow - mTransitionDuration)); + } else if ((timeAfterEndShow = timeAfterStartShow - mTransitionDuration - mKeepShownTime) + < mTransitionDuration) { + // in hide animation + mCurrentAlpha = 1 - timeAfterEndShow * 1f / mTransitionDuration; + needInvalidate = 0; + } else { + mCurrentAlpha = 0f; + } + if (mCurrentAlpha <= 0f) { + return; + } + drawable.setAlpha((int) (mCurrentAlpha * 255)); + int totalHeight = getHeight(), totalWidth = getWidth(); + int top = (int) ((totalHeight - drawableHeight) * mPercent); + int left = totalWidth - drawableWidth; + drawable.setBounds(0, 0, drawableWidth, drawableHeight); + canvas.save(); + canvas.translate(left, top); + drawable.draw(canvas); + canvas.restore(); + + if (needInvalidate == 0) { + invalidate(); + } else if (needInvalidate > 0) { + ViewCompat.postOnAnimationDelayed(this, mDelayInvalidateRunnable, needInvalidate); + } + } + + interface Callback { + void onDragToPercent(float percent); + } +} diff --git a/qmui/src/main/res/drawable-xhdpi/qmui_icon_scroll_bar.png b/qmui/src/main/res/drawable-xhdpi/qmui_icon_scroll_bar.png new file mode 100644 index 0000000000000000000000000000000000000000..16708862d22fcf70a2ca9f521b8de1cfbbc8529a GIT binary patch literal 2817 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