diff --git a/recyclerviewfastscroller/src/main/java/com/qtalk/recyclerviewfastscroller/RecyclerViewFastScroller.kt b/recyclerviewfastscroller/src/main/java/com/qtalk/recyclerviewfastscroller/RecyclerViewFastScroller.kt index 5b1164e..298070e 100644 --- a/recyclerviewfastscroller/src/main/java/com/qtalk/recyclerviewfastscroller/RecyclerViewFastScroller.kt +++ b/recyclerviewfastscroller/src/main/java/com/qtalk/recyclerviewfastscroller/RecyclerViewFastScroller.kt @@ -26,11 +26,8 @@ import android.graphics.drawable.Drawable import android.os.Build import android.util.AttributeSet import android.util.Log -import android.view.Gravity -import android.view.MotionEvent -import android.view.View +import android.view.* import android.view.View.OnTouchListener -import android.view.ViewPropertyAnimator import android.widget.LinearLayout import android.widget.RelativeLayout import android.widget.TextView @@ -87,7 +84,7 @@ class RecyclerViewFastScroller @JvmOverloads constructor( "The RecyclerView required for initialization with FastScroller cannot be null" } - private enum class FastScrollDirection(val value: Int) { + enum class FastScrollDirection(val value: Int) { HORIZONTAL(1), VERTICAL(0); companion object { @@ -127,6 +124,8 @@ class RecyclerViewFastScroller @JvmOverloads constructor( internal const val DEFAULT_ANIM_DURATION: Long = 100 internal const val DEFAULT_POPUP_VISIBILITY_DURATION = 200L internal const val hasEmptyItemDecorator: Boolean = true + internal const val trackMargin: Int = 0 + internal const val disableTrack = false } /** @@ -180,9 +179,38 @@ class RecyclerViewFastScroller @JvmOverloads constructor( **/ lateinit var popupTextView: TextView + var trackMarginStart: Int = 0 + set(value) { + field = value + setTrackMargin() + } + + var trackMarginEnd: Int = 0 + set(value) { + field = value + setTrackMargin() + } + + var fastScrollDirection: FastScrollDirection = Defaults.fastScrollDirection + set(value) { + field = value + alignTrackAndHandle() + } + + var handleWidth: Int = LayoutParams.WRAP_CONTENT + set(value) { + field = value + refreshHandleImageViewSize() + } + var handleHeight: Int = LayoutParams.WRAP_CONTENT + set(value) { + field = value + refreshHandleImageViewSize() + } + var disableTrack: Boolean = Defaults.disableTrack + // --- internal properties private var popupPosition: PopupPosition = Defaults.popupPosition - private var fastScrollDirection: FastScrollDirection = Defaults.fastScrollDirection private var hasEmptyItemDecorator: Boolean = Defaults.hasEmptyItemDecorator private lateinit var handleImageView: AppCompatImageView private lateinit var trackView: LinearLayout @@ -192,6 +220,33 @@ class RecyclerViewFastScroller @JvmOverloads constructor( private var handleStateListener: HandleStateListener? = null private var previousTotalVisibleItem: Int = 0 + private val trackLength: Float + get() = + when (fastScrollDirection) { + FastScrollDirection.VERTICAL -> + trackView.height + FastScrollDirection.HORIZONTAL -> + trackView.width + }.toFloat() + + private val handleLength: Float + get() = + when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> + handleImageView.width + FastScrollDirection.VERTICAL -> + handleImageView.height + }.toFloat() + + private val popupLength: Float + get() = + when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> + popupTextView.width + FastScrollDirection.VERTICAL -> + popupTextView.height + }.toFloat() + // property check /** * Checks if the [FastScrollDirection] is [FastScrollDirection.VERTICAL] or not @@ -265,12 +320,21 @@ class RecyclerViewFastScroller @JvmOverloads constructor( } // set default handleImageView drawable if not defined - handleImageView.setImageDrawable( - (loadDrawableFromAttributes(R.styleable.RecyclerViewFastScroller_handleDrawable) + handleDrawable = (loadDrawableFromAttributes(R.styleable.RecyclerViewFastScroller_handleDrawable) ?: ContextCompat.getDrawable(context, Defaults.handleDrawableInt)) - ) - refreshHandleImageViewSize() + handleHeight = + attribs.getDimensionPixelSize(R.styleable.RecyclerViewFastScroller_handleHeight, loadDimenFromResource(Defaults.handleSize)) + handleWidth = + attribs.getDimensionPixelSize(R.styleable.RecyclerViewFastScroller_handleWidth, loadDimenFromResource(Defaults.handleSize)) + + trackMarginStart = + attribs.getDimensionPixelSize(R.styleable.RecyclerViewFastScroller_trackMarginStart, Defaults.trackMargin) + trackMarginEnd = + attribs.getDimensionPixelSize(R.styleable.RecyclerViewFastScroller_trackMarginEnd, Defaults.trackMargin) + + disableTrack = + attribs.getBoolean(R.styleable.RecyclerViewFastScroller_disableTrack, Defaults.disableTrack) TextViewCompat.setTextAppearance( popupTextView, @@ -305,19 +369,35 @@ class RecyclerViewFastScroller @JvmOverloads constructor( } } post { - val locationArray = IntArray(2) - - // getting the position of this view on the screen, getting absolute X and Y coordinates - getLocationInWindow(locationArray) - val yAbsPosition: Int = locationArray[1] - val touchListener = OnTouchListener { _, motionEvent -> + val locationArray = IntArray(2) + + // getting the position of this view on the screen, getting absolute X and Y coordinates + trackView.getLocationInWindow(locationArray) + val (xAbsPosition, yAbsPosition) = Pair(locationArray[0], locationArray[1]) val touchAction = motionEvent.action.and(motionEvent.actionMasked) log("Touch Action: $touchAction") when (touchAction) { MotionEvent.ACTION_MOVE, MotionEvent.ACTION_DOWN -> { + val handlePosition = IntArray(2).also { + handleImageView.getLocationOnScreen(it) + } + if (disableTrack) { + when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> { + val handleRange = handlePosition[0].toFloat() .. handlePosition[0]+handleLength + if (!handleRange.contains(motionEvent.rawX)) + return@OnTouchListener false + } + FastScrollDirection.VERTICAL -> { + val handleRange = handlePosition[1].toFloat() .. handlePosition[1]+handleLength + if (!handleRange.contains(motionEvent.rawY)) + return@OnTouchListener false + } + } + } // disallow parent to spy on touch events requestDisallowInterceptTouchEvent(true) @@ -343,23 +423,31 @@ class RecyclerViewFastScroller @JvmOverloads constructor( // // subtract the handle height offset - val handleHeightOffset = handleImageView.height / 2 + val handleOffset = handleLength / 2 - val currentRelativeYPos = - motionEvent.rawY - yAbsPosition - handleHeightOffset + val currentRelativePos = when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> + motionEvent.rawX - xAbsPosition - handleOffset + FastScrollDirection.VERTICAL -> + motionEvent.rawY - yAbsPosition - handleOffset + } // move the handle only if fastScrolled, else leave the translation of the handle to the onScrolled method on the listener if (isFastScrollEnabled) { - moveViewByRelativeYInBounds(handleImageView, currentRelativeYPos) - moveViewByRelativeYInBounds( + moveViewByRelativeInBounds(handleImageView, currentRelativePos) + moveViewByRelativeInBounds( popupTextView, - currentRelativeYPos - popupTextView.height + currentRelativePos - popupLength ) val position = - recyclerView.computePositionForOffsetAndScroll(currentRelativeYPos) + recyclerView.computePositionForOffsetAndScroll(currentRelativePos) if (motionEvent.action == MotionEvent.ACTION_MOVE) { - handleStateListener?.onDragged(handleImageView.y, position) + handleStateListener?.onDragged( + when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> handleImageView.x + FastScrollDirection.VERTICAL -> handleImageView.y + }, position) } updateTextInPopup( min( @@ -368,7 +456,12 @@ class RecyclerViewFastScroller @JvmOverloads constructor( ) ) } else { - recyclerView.scrollBy(0, currentRelativeYPos.toInt()) + when ((recyclerView.layoutManager as LinearLayoutManager).orientation) { + RecyclerView.HORIZONTAL -> + recyclerView.scrollBy(currentRelativePos.toInt(), 0) + RecyclerView.VERTICAL -> + recyclerView.scrollBy(0, currentRelativePos.toInt()) + } } true @@ -402,16 +495,28 @@ class RecyclerViewFastScroller @JvmOverloads constructor( LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).also { when (popupPosition) { PopupPosition.BEFORE_TRACK -> { - if (Build.VERSION.SDK_INT > 16) - it.addRule(START_OF, trackView.id) - else - it.addRule(LEFT_OF, trackView.id) + when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> + it.addRule(ABOVE, trackView.id) + FastScrollDirection.VERTICAL -> { + if (Build.VERSION.SDK_INT > 16) + it.addRule(START_OF, trackView.id) + else + it.addRule(LEFT_OF, trackView.id) + } + } } PopupPosition.AFTER_TRACK -> { - if (Build.VERSION.SDK_INT > 16) - it.addRule(END_OF, trackView.id) - else - it.addRule(RIGHT_OF, trackView.id) + when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> + it.addRule(BELOW, trackView.id) + FastScrollDirection.VERTICAL -> { + if (Build.VERSION.SDK_INT > 16) + it.addRule(END_OF, trackView.id) + else + it.addRule(RIGHT_OF, trackView.id) + } + } } } } @@ -419,18 +524,18 @@ class RecyclerViewFastScroller @JvmOverloads constructor( } private fun alignTrackAndHandle() { - val lp = LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) - val lpTrackLayout: LayoutParams = when (fastScrollDirection) { + val padding = resources.getDimensionPixelOffset(R.dimen.default_handle_padding) + when (fastScrollDirection) { FastScrollDirection.HORIZONTAL -> { - lp.gravity = Gravity.END - LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.WRAP_CONTENT - ).also { it.addRule(ALIGN_PARENT_BOTTOM) } + handleImageView.setPadding(0, padding, 0, padding) + trackView.layoutParams = LayoutParams( + LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT + ).also { it.addRule(ALIGN_PARENT_BOTTOM) } } FastScrollDirection.VERTICAL -> { - lp.gravity = Gravity.TOP - LayoutParams( + handleImageView.setPadding(padding, 0, padding, 0) + trackView.layoutParams = LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT ).also { @@ -441,15 +546,37 @@ class RecyclerViewFastScroller @JvmOverloads constructor( } } } - handleImageView.layoutParams = lp - trackView.layoutParams = lpTrackLayout + post { + when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> + handleImageView.y = 0F + FastScrollDirection.VERTICAL -> + handleImageView.x = 0F + } + + onScrollListener.onScrolled(recyclerView, 0, 0) + } + } + + private fun setTrackMargin() { + with (trackView.layoutParams as MarginLayoutParams) { + when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> + if (Build.VERSION.SDK_INT > 16) { + marginStart = trackMarginStart + marginEnd = trackMarginEnd + } else + setMargins(trackMarginStart, 0, trackMarginEnd, 0) + FastScrollDirection.VERTICAL -> + setMargins(0, trackMarginStart, 0, trackMarginEnd) + } + } } private fun refreshHandleImageViewSize(newComputedSize: Int = -1) { // todo@shahsurajk add fork for horizontal layout if (newComputedSize == -1) { - handleImageView.layoutParams.width = loadHandleWidth().toInt() - handleImageView.layoutParams.height = loadHandleHeight().toInt() + handleImageView.layoutParams = LinearLayout.LayoutParams(handleWidth, handleHeight) } else { TODO("@shahsurajk dynamic sizing of handle") } @@ -482,8 +609,13 @@ class RecyclerViewFastScroller @JvmOverloads constructor( * @param view the view to move and * @param finalOffset the offset to move to * */ - private fun moveViewByRelativeYInBounds(view: View, finalOffset: Float) { - view.y = min(max(finalOffset, 0f), (height.toFloat() - view.height.toFloat())) + private fun moveViewByRelativeInBounds(view: View, finalOffset: Float) { + when (fastScrollDirection) { + FastScrollDirection.HORIZONTAL -> + view.x = min(max(finalOffset, 0F), (trackLength - view.width.toFloat())) + FastScrollDirection.VERTICAL -> + view.y = min(max(finalOffset, 0F), (trackLength - view.height.toFloat())) + } } /** @@ -525,22 +657,8 @@ class RecyclerViewFastScroller @JvmOverloads constructor( } // set of load methods for handy loading from attribs - private fun loadDimenFromResource(@DimenRes dimenSize: Int): Float = - context.resources.getDimension(dimenSize) - - private fun loadHandleHeight() = - attribs?.getDimension( - R.styleable.RecyclerViewFastScroller_handleHeight, - loadDimenFromResource(Defaults.handleSize) - ) - ?: loadDimenFromResource(Defaults.handleSize) - - private fun loadHandleWidth() = - attribs?.getDimension( - R.styleable.RecyclerViewFastScroller_handleWidth, - loadDimenFromResource(Defaults.handleSize) - ) - ?: loadDimenFromResource(Defaults.handleSize) + private fun loadDimenFromResource(@DimenRes dimenSize: Int): Int = + context.resources.getDimensionPixelSize(dimenSize) private fun loadDrawableFromAttributes(@StyleableRes styleId: Int) = attribs?.getDrawable(styleId) @@ -592,8 +710,8 @@ class RecyclerViewFastScroller @JvmOverloads constructor( private fun RecyclerView.computePositionForOffsetAndScroll(relativeRawPos: Float): Int { val layoutManager: RecyclerView.LayoutManager? = this.layoutManager val recyclerViewItemCount = this.adapter?.itemCount ?: 0 - val newOffset = relativeRawPos / ((this.computeVerticalScrollExtent() - .toFloat()) - handleImageView.height.toFloat()) + + val newOffset = relativeRawPos / (trackLength - handleLength) return when (layoutManager) { is LinearLayoutManager -> { val totalVisibleItems = layoutManager.getTotalCompletelyVisibleItemCount() @@ -603,18 +721,24 @@ class RecyclerViewFastScroller @JvmOverloads constructor( // the last item would have one less visible item, this is to offset it. previousTotalVisibleItem = max(previousTotalVisibleItem, totalVisibleItems) // check bounds and then set position - val position = min( - recyclerViewItemCount, - max(0, (newOffset * (recyclerViewItemCount - totalVisibleItems)).roundToInt()) - ) + val position = + if (layoutManager.reverseLayout) + min( + recyclerViewItemCount, + max(0, recyclerViewItemCount-(newOffset * (recyclerViewItemCount - totalVisibleItems)).roundToInt()) + ) + else + min( + recyclerViewItemCount, + max(0, (newOffset * (recyclerViewItemCount - totalVisibleItems)).roundToInt()) + ) val toScrollPosition = - min((this.adapter?.itemCount ?: 0) - (previousTotalVisibleItem + 1), position) + min((this.adapter?.itemCount ?: 0) - (previousTotalVisibleItem + 1), position) safeScrollToPosition(toScrollPosition) position } else -> { - val position = (newOffset * recyclerViewItemCount).roundToInt() safeScrollToPosition(position) position @@ -697,13 +821,17 @@ class RecyclerViewFastScroller @JvmOverloads constructor( super.onScrolled(recyclerView, dx, dy) if (isEngaged && isFastScrollEnabled) return - val computeVerticalScrollExtent: Float = - recyclerView.computeVerticalScrollExtent().toFloat() - val computeVerticalScrollRange: Float = - recyclerView.computeVerticalScrollRange().toFloat() + val (range, extent, offset) = + when ((recyclerView.layoutManager as LinearLayoutManager).orientation) { + RecyclerView.HORIZONTAL -> + Triple(recyclerView.computeHorizontalScrollRange(), recyclerView.computeHorizontalScrollExtent(), recyclerView.computeHorizontalScrollOffset()) + RecyclerView.VERTICAL -> + Triple(recyclerView.computeVerticalScrollRange(), recyclerView.computeVerticalScrollExtent(), recyclerView.computeVerticalScrollOffset()) + else -> error("The orientation of the LinearLayoutManager should be horizontal or vertical") + } // check if the layout is scrollable. i.e. range is large than extent, else disable fast scrolling and track touches. - if (computeVerticalScrollExtent < computeVerticalScrollRange) { + if (extent < range) { handleImageView.animateVisibility() handleImageView.isEnabled = true trackView.isEnabled = true @@ -713,12 +841,12 @@ class RecyclerViewFastScroller @JvmOverloads constructor( handleImageView.isEnabled = false return } - val offsetScale = (recyclerView.computeVerticalScrollOffset() - .toFloat()) / ((computeVerticalScrollRange) - (computeVerticalScrollExtent)) - val finalOffset = - offsetScale * (computeVerticalScrollExtent - handleImageView.height.toFloat()) - moveViewByRelativeYInBounds(handleImageView, finalOffset) - moveViewByRelativeYInBounds(popupTextView, finalOffset - popupTextView.height.toFloat()) + + val error = extent.toFloat()*offset/range + val finalOffset: Float = (trackLength - handleLength) * ((error+offset)/range) + + moveViewByRelativeInBounds(handleImageView, finalOffset) + moveViewByRelativeInBounds(popupTextView, finalOffset - popupLength) } } diff --git a/recyclerviewfastscroller/src/main/res/values/attrs.xml b/recyclerviewfastscroller/src/main/res/values/attrs.xml index bf4a36b..7b220ac 100644 --- a/recyclerviewfastscroller/src/main/res/values/attrs.xml +++ b/recyclerviewfastscroller/src/main/res/values/attrs.xml @@ -19,5 +19,10 @@ + + + + + \ No newline at end of file diff --git a/recyclerviewfastscroller/src/main/res/values/dimens.xml b/recyclerviewfastscroller/src/main/res/values/dimens.xml index b4330ac..8c38477 100644 --- a/recyclerviewfastscroller/src/main/res/values/dimens.xml +++ b/recyclerviewfastscroller/src/main/res/values/dimens.xml @@ -4,4 +4,6 @@ 18dp 16dp + + 4dp \ No newline at end of file