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 00000000..16708862 Binary files /dev/null and b/qmui/src/main/res/drawable-xhdpi/qmui_icon_scroll_bar.png differ diff --git a/qmui/src/main/res/drawable-xxhdpi/qmui_icon_scroll_bar.png b/qmui/src/main/res/drawable-xxhdpi/qmui_icon_scroll_bar.png new file mode 100644 index 00000000..a79350f8 Binary files /dev/null and b/qmui/src/main/res/drawable-xxhdpi/qmui_icon_scroll_bar.png differ diff --git a/qmui/src/main/res/drawable-xxxhdpi/qmui_icon_scroll_bar.png b/qmui/src/main/res/drawable-xxxhdpi/qmui_icon_scroll_bar.png new file mode 100644 index 00000000..94b9d7e6 Binary files /dev/null and b/qmui/src/main/res/drawable-xxxhdpi/qmui_icon_scroll_bar.png differ