diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..603b140
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..fb7f4a8
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 0000000..b11a301
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..a5f05cd
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..6199cc2
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bouncy/.gitignore b/bouncy/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/bouncy/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/bouncy/build.gradle b/bouncy/build.gradle
new file mode 100644
index 0000000..7f13f16
--- /dev/null
+++ b/bouncy/build.gradle
@@ -0,0 +1,38 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.2"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation 'androidx.core:core-ktx:1.3.2'
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation "androidx.dynamicanimation:dynamicanimation:1.0.0"
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ testImplementation 'junit:junit:4.13.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+
+}
\ No newline at end of file
diff --git a/bouncy/consumer-rules.pro b/bouncy/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/bouncy/proguard-rules.pro b/bouncy/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/bouncy/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/bouncy/src/androidTest/java/com/factor/bouncy/ExampleInstrumentedTest.kt b/bouncy/src/androidTest/java/com/factor/bouncy/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..4474e2c
--- /dev/null
+++ b/bouncy/src/androidTest/java/com/factor/bouncy/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.factor.bouncy
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.factor.bouncy.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/bouncy/src/main/AndroidManifest.xml b/bouncy/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0118483
--- /dev/null
+++ b/bouncy/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/bouncy/src/main/java/com/factor/bouncy/BouncyNestedScrollView.kt b/bouncy/src/main/java/com/factor/bouncy/BouncyNestedScrollView.kt
new file mode 100644
index 0000000..a0955a7
--- /dev/null
+++ b/bouncy/src/main/java/com/factor/bouncy/BouncyNestedScrollView.kt
@@ -0,0 +1,1909 @@
+package com.factor.bouncy
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcel
+import android.os.Parcelable
+import android.os.Parcelable.Creator
+import android.util.AttributeSet
+import android.util.Log
+import android.util.TypedValue
+import android.view.*
+import android.view.accessibility.AccessibilityEvent
+import android.view.animation.AnimationUtils
+import android.widget.EdgeEffect
+import android.widget.FrameLayout
+import android.widget.OverScroller
+import android.widget.ScrollView
+import androidx.annotation.RestrictTo
+import androidx.core.view.*
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.core.view.accessibility.AccessibilityRecordCompat
+import androidx.core.widget.EdgeEffectCompat
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory
+import com.factor.bouncy.util.BouncyEdgeEffect
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+
+/**
+ * Modified based on NestedScrollView source code
+ */
+
+@Suppress("MemberVisibilityCanBePrivate", "unused")
+class BouncyNestedScrollView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
+ : FrameLayout(context, attrs, defStyleAttr),
+ NestedScrollingParent3,
+ NestedScrollingChild3,
+ ScrollingView
+{
+
+ interface OnScrollChangeListener
+ {
+ fun onScrollChange(v: BouncyNestedScrollView?, scrollX: Int, scrollY: Int, oldScrollX: Int, oldScrollY: Int)
+ }
+
+ private var mLastScroll: Long = 0
+ private val mTempRect = Rect()
+ private var mScroller: OverScroller? = null
+ private var mEdgeGlowTop: EdgeEffect? = null
+ private var mEdgeGlowBottom: EdgeEffect? = null
+
+ /**
+ * Position of the last motion event.
+ */
+ private var mLastMotionY = 0
+
+ /**
+ * True when the layout has changed but the traversal has not come through yet.
+ * Ideally the view hierarchy would keep track of this for us.
+ */
+ private var mIsLayoutDirty = true
+ private var mIsLaidOut = false
+
+ /**
+ * The child to give focus to in the event that a child has requested focus while the
+ * layout is dirty. This prevents the scroll from being wrong if the child has not been
+ * laid out before requesting focus.
+ */
+ private var mChildToScrollTo: View? = null
+
+ /**
+ * True if the user is currently dragging this ScrollView around. This is
+ * not the same as 'is being flung', which can be checked by
+ * mScroller.isFinished() (flinging begins when the user lifts his finger).
+ */
+ private var mIsBeingDragged = false
+
+ /**
+ * Determines speed during touch scrolling
+ */
+ private var mVelocityTracker: VelocityTracker? = null
+
+ /**
+ * When set to true, the scroll view measure its child to make it fill the currently
+ * visible area.
+ */
+ private var mFillViewport = false
+ /**
+ * Whether arrow scrolling is animated.
+ */
+ var isSmoothScrollingEnabled = true
+ private var mTouchSlop = 0
+ private var mMinimumVelocity = 0
+ private var mMaximumVelocity = 0
+
+
+ var overscrollAnimationSize = 0.5f
+ var flingAnimationSize = 0.5f
+
+ /**
+ * ID of the active pointer. This is used to retain consistency during
+ * drags/flings if multiple pointers are used.
+ */
+ private var mActivePointerId = INVALID_POINTER
+
+ /**
+ * Used during scrolling to retrieve the new offset within the window.
+ */
+ private val mScrollOffset = IntArray(2)
+ private val mScrollConsumed = IntArray(2)
+ private var mNestedYOffset = 0
+ private var mSavedState: SavedState? = null
+ private val mParentHelper: NestedScrollingParentHelper
+ private val mChildHelper: NestedScrollingChildHelper
+ private var mVerticalScrollFactor = 0f
+ var mOnScrollChangeListener: OnScrollChangeListener? = null
+
+ private val spring = SpringAnimation(this, SpringAnimation.TRANSLATION_Y)
+ .setSpring(
+ SpringForce()
+ .setFinalPosition(0f)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
+ .setStiffness(SpringForce.STIFFNESS_LOW)
+ )
+
+
+ // NestedScrollingChild3
+ override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?, type: Int, consumed: IntArray)
+ = mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type, consumed)
+
+
+ // NestedScrollingChild2
+ override fun startNestedScroll(axes: Int, type: Int): Boolean
+ = mChildHelper.startNestedScroll(axes, type)
+
+
+ override fun stopNestedScroll(type: Int)
+ = mChildHelper.stopNestedScroll(type)
+
+
+ override fun hasNestedScrollingParent(type: Int): Boolean
+ = mChildHelper.hasNestedScrollingParent(type)
+
+
+ override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?, type: Int): Boolean
+ = mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow, type)
+
+ override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean
+ = mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
+
+
+ // NestedScrollingChild
+ override fun setNestedScrollingEnabled(enabled: Boolean)
+ {
+ mChildHelper.isNestedScrollingEnabled = enabled
+ }
+
+ override fun isNestedScrollingEnabled(): Boolean
+ = mChildHelper.isNestedScrollingEnabled
+
+ override fun startNestedScroll(axes: Int): Boolean
+ = startNestedScroll(axes, ViewCompat.TYPE_TOUCH)
+
+ override fun stopNestedScroll()
+ = stopNestedScroll(ViewCompat.TYPE_TOUCH)
+
+ override fun hasNestedScrollingParent(): Boolean
+ = hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)
+
+ override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?): Boolean
+ = mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
+
+ override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?): Boolean
+ = dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, ViewCompat.TYPE_TOUCH)
+
+ override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean): Boolean
+ = mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed)
+
+ override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean
+ = mChildHelper.dispatchNestedPreFling(velocityX, velocityY)
+
+ // NestedScrollingParent3
+ override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int, consumed: IntArray)
+ = onNestedScrollInternal(dyUnconsumed, type, consumed)
+
+ private fun onNestedScrollInternal(dyUnconsumed: Int, type: Int, consumed: IntArray?)
+ {
+ val oldScrollY = scrollY
+ scrollBy(0, dyUnconsumed)
+ val myConsumed = scrollY - oldScrollY
+ if (consumed != null)
+ {
+ consumed[1] += myConsumed
+ }
+ val myUnconsumed = dyUnconsumed - myConsumed
+ mChildHelper.dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null, type, consumed)
+ }
+
+ // NestedScrollingParent2
+ override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean = axes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
+
+ override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int)
+ {
+ mParentHelper.onNestedScrollAccepted(child, target, axes, type)
+ startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, type)
+ }
+
+ override fun onStopNestedScroll(target: View, type: Int)
+ {
+ mParentHelper.onStopNestedScroll(target, type)
+ stopNestedScroll(type)
+ }
+
+ override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int)
+ = onNestedScrollInternal(dyUnconsumed, type, null)
+
+ override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int)
+ {
+ dispatchNestedPreScroll(dx, dy, consumed, null, type)
+ }
+
+ // NestedScrollingParent
+ override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean
+ = onStartNestedScroll(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH)
+
+ override fun onNestedScrollAccepted(child: View, target: View, nestedScrollAxes: Int)
+ = onNestedScrollAccepted(child, target, nestedScrollAxes, ViewCompat.TYPE_TOUCH)
+
+ override fun onStopNestedScroll(target: View)
+ = onStopNestedScroll(target, ViewCompat.TYPE_TOUCH)
+
+ override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int)
+ = onNestedScrollInternal(dyUnconsumed, ViewCompat.TYPE_TOUCH, null)
+
+ override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray)
+ = onNestedPreScroll(target, dx, dy, consumed, ViewCompat.TYPE_TOUCH)
+
+ override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean
+ {
+ return if (!consumed)
+ {
+ dispatchNestedFling(0f, velocityY, true)
+ fling(velocityY.toInt())
+ true
+ }
+ else false
+ }
+
+ override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean
+ = dispatchNestedPreFling(velocityX, velocityY)
+
+ override fun getNestedScrollAxes(): Int
+ = mParentHelper.nestedScrollAxes
+
+ // ScrollView import
+ override fun shouldDelayChildPressedState(): Boolean = true
+
+ override fun getTopFadingEdgeStrength(): Float
+ {
+ if (childCount == 0) return 0.0f
+
+ val length = verticalFadingEdgeLength
+ val scrollY = scrollY
+ return if (scrollY < length)
+ scrollY / length.toFloat()
+ else 1.0f
+ }
+
+ override fun getBottomFadingEdgeStrength(): Float
+ {
+ if (childCount == 0) return 0.0f
+
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ val length = verticalFadingEdgeLength
+ val bottomEdge = height - paddingBottom
+ val span = child.bottom + lp.bottomMargin - scrollY - bottomEdge
+ return if (span < length)
+ span / length.toFloat()
+ else 1.0f
+ }
+
+ /**
+ * @return The maximum amount this scroll view will scroll in response to
+ * an arrow event.
+ */
+ @Suppress("MemberVisibilityCanBePrivate")
+ val maxScrollAmount: Int
+ get() = (MAX_SCROLL_FACTOR * height).toInt()
+
+ private fun initScrollView()
+ {
+ mScroller = OverScroller(context)
+ isFocusable = true
+ descendantFocusability = FOCUS_AFTER_DESCENDANTS
+ setWillNotDraw(false)
+ val configuration = ViewConfiguration.get(context)
+ mTouchSlop = configuration.scaledTouchSlop
+ mMinimumVelocity = configuration.scaledMinimumFlingVelocity
+ mMaximumVelocity = configuration.scaledMaximumFlingVelocity
+ }
+
+ override fun addView(child: View)
+ {
+ check(childCount <= 0) { "ScrollView can host only one direct child" }
+ super.addView(child)
+ }
+
+ override fun addView(child: View, index: Int)
+ {
+ check(childCount <= 0) { "ScrollView can host only one direct child" }
+ super.addView(child, index)
+ }
+
+ override fun addView(child: View, params: ViewGroup.LayoutParams)
+ {
+ check(childCount <= 0) { "ScrollView can host only one direct child" }
+ super.addView(child, params)
+ }
+
+ override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams)
+ {
+ check(childCount <= 0) { "ScrollView can host only one direct child" }
+ super.addView(child, index, params)
+ }
+
+
+ /**
+ * @return Returns true this ScrollView can be scrolled
+ */
+ private fun canScroll(): Boolean
+ {
+ return if (childCount > 0)
+ {
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ val childSize = child.height + lp.topMargin + lp.bottomMargin
+ val parentSpace = height - paddingTop - paddingBottom
+ childSize > parentSpace
+ }
+ else false
+ }
+
+ var isFillViewport: Boolean
+ get() = mFillViewport
+ set(fillViewport)
+ {
+ @Suppress("unused")
+ if (fillViewport != mFillViewport)
+ {
+ mFillViewport = fillViewport
+ requestLayout()
+ }
+ }
+
+ override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int)
+ {
+ super.onScrollChanged(l, t, oldl, oldt)
+ mOnScrollChangeListener?.onScrollChange(this, l, t, oldl, oldt)
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)
+ {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ if (mFillViewport)
+ {
+ val heightMode = MeasureSpec.getMode(heightMeasureSpec)
+ if (heightMode != MeasureSpec.UNSPECIFIED)
+ {
+ if (childCount > 0)
+ {
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ val childSize = child.measuredHeight
+ val parentSpace = (measuredHeight - paddingTop - paddingBottom - lp.topMargin - lp.bottomMargin)
+ if (childSize < parentSpace)
+ {
+ val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight + lp.leftMargin + lp.rightMargin, lp.width)
+ val childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentSpace, MeasureSpec.EXACTLY)
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
+ }
+ }
+ }
+ }
+ }
+
+ // Let the focused view and/or our descendants get the key first
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean
+ = super.dispatchKeyEvent(event) || executeKeyEvent(event)
+
+ /**
+ * You can call this function yourself to have the scroll view perform
+ * scrolling from a key event, just as if the event had been dispatched to
+ * it by the view hierarchy.
+ *
+ * @param event The key event to execute.
+ * @return Return true if the event was handled, else false.
+ */
+ fun executeKeyEvent(event: KeyEvent): Boolean
+ {
+ mTempRect.setEmpty()
+ if (!canScroll())
+ {
+ return if (isFocused && event.keyCode != KeyEvent.KEYCODE_BACK)
+ {
+ var currentFocused = findFocus()
+ if (currentFocused === this)
+ currentFocused = null
+ val nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, FOCUS_DOWN)
+ nextFocused != null && nextFocused !== this && nextFocused.requestFocus(FOCUS_DOWN)
+ }
+ else false
+ }
+ var handled = false
+ if (event.action == KeyEvent.ACTION_DOWN)
+ {
+ when (event.keyCode)
+ {
+ KeyEvent.KEYCODE_DPAD_UP -> handled =
+ if (!event.isAltPressed)
+ arrowScroll(FOCUS_UP)
+ else
+ fullScroll(FOCUS_UP)
+
+ KeyEvent.KEYCODE_DPAD_DOWN -> handled =
+ if (!event.isAltPressed)
+ arrowScroll(FOCUS_DOWN)
+ else
+ fullScroll(FOCUS_DOWN)
+
+ KeyEvent.KEYCODE_SPACE -> pageScroll(if (event.isShiftPressed) FOCUS_UP else FOCUS_DOWN)
+ }
+ }
+ return handled
+ }
+
+ private fun inChild(x: Int, y: Int): Boolean
+ {
+ if (childCount > 0)
+ {
+ val scrollY = scrollY
+ val child = getChildAt(0)
+ return !(y < child.top - scrollY || y >= child.bottom - scrollY || x < child.left || x >= child.right)
+ }
+ return false
+ }
+
+ private fun initOrResetVelocityTracker()
+ {
+ if (mVelocityTracker == null)
+ mVelocityTracker = VelocityTracker.obtain()
+ else
+ mVelocityTracker!!.clear()
+
+ }
+
+ private fun initVelocityTrackerIfNotExists()
+ {
+ if (mVelocityTracker == null)
+ mVelocityTracker = VelocityTracker.obtain()
+ }
+
+ private fun recycleVelocityTracker()
+ {
+ if (mVelocityTracker != null)
+ {
+ mVelocityTracker!!.recycle()
+ mVelocityTracker = null
+ }
+ }
+
+ override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean)
+ {
+ if (disallowIntercept)
+ recycleVelocityTracker()
+ super.requestDisallowInterceptTouchEvent(disallowIntercept)
+ }
+
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean
+ {
+ /*
+ * This method JUST determines whether we want to intercept the motion.
+ * If we return true, onMotionEvent will be called and we do the actual
+ * scrolling there.
+ */
+
+ /*
+ * Shortcut the most recurring case: the user is in the dragging
+ * state and he is moving his finger. We want to intercept this
+ * motion.
+ */
+ val action = ev.action
+
+ if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged)
+ return true
+
+ when (action and MotionEvent.ACTION_MASK)
+ {
+ MotionEvent.ACTION_MOVE ->
+ {
+
+ /*
+ * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+ * whether the user has moved far enough from his original down touch.
+ */
+
+ /*
+ * Locally do absolute value. mLastMotionY is set to the y value
+ * of the down event.
+ */
+ val activePointerId = mActivePointerId
+ if (activePointerId != INVALID_POINTER)
+ {
+ // If we don't have a valid id, the touch down wasn't on content.
+ val pointerIndex = ev.findPointerIndex(activePointerId)
+
+ if (pointerIndex == -1)
+ Log.e(TAG, "Invalid pointerId=$activePointerId in onInterceptTouchEvent")
+
+ val y = ev.getY(pointerIndex).toInt()
+ val yDiff = abs(y - mLastMotionY)
+ if (yDiff > mTouchSlop && nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL == 0)
+ {
+ mIsBeingDragged = true
+ mLastMotionY = y
+ initVelocityTrackerIfNotExists()
+ mVelocityTracker!!.addMovement(ev)
+ mNestedYOffset = 0
+ val parent = parent
+ parent?.requestDisallowInterceptTouchEvent(true)
+ }
+ }
+ }
+ MotionEvent.ACTION_DOWN ->
+ {
+ val y = ev.y.toInt()
+ if (!inChild(ev.x.toInt(), y))
+ {
+ mIsBeingDragged = false
+ recycleVelocityTracker()
+ }
+
+ /*
+ * Remember location of down touch.
+ * ACTION_DOWN always refers to pointer index 0.
+ */
+
+ mLastMotionY = y
+ mActivePointerId = ev.getPointerId(0)
+ initOrResetVelocityTracker()
+ mVelocityTracker!!.addMovement(ev)
+
+ /*
+ * If being flung
+ * and user touches the screen, initiate drag;
+ * otherwise don't. mScroller.isFinished should be false when
+ * being flung
+ * . We need to call computeScrollOffset() first so that
+ * isFinished() is correct.
+ */
+
+ mScroller!!.computeScrollOffset()
+ mIsBeingDragged = !mScroller!!.isFinished
+ startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
+ }
+ MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP ->
+ {
+ /* Release the drag */mIsBeingDragged = false
+ mActivePointerId = INVALID_POINTER
+ recycleVelocityTracker()
+ if (mScroller!!.springBack(scrollX, scrollY, 0, 0, 0, scrollRange))
+ ViewCompat.postInvalidateOnAnimation(this)
+
+ stopNestedScroll(ViewCompat.TYPE_TOUCH)
+ }
+
+ MotionEvent.ACTION_POINTER_UP -> onSecondaryPointerUp(ev)
+ }
+
+ /*
+ * The only time we want to intercept motion events is if we are in the
+ * drag mode.
+ */
+
+ return mIsBeingDragged
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(ev: MotionEvent): Boolean
+ {
+ initVelocityTrackerIfNotExists()
+ val actionMasked = ev.actionMasked
+
+ if (actionMasked == MotionEvent.ACTION_DOWN)
+ mNestedYOffset = 0
+
+ val vMotionEvent = MotionEvent.obtain(ev)
+ vMotionEvent.offsetLocation(0f, mNestedYOffset.toFloat())
+ when (actionMasked)
+ {
+ MotionEvent.ACTION_DOWN ->
+ {
+ if (childCount == 0)
+ return false
+
+ if (!mScroller!!.isFinished.also { mIsBeingDragged = it })
+ {
+ val parent = parent
+ parent?.requestDisallowInterceptTouchEvent(true)
+ }
+
+ /*
+ * If being flung
+ * and user touches, stop the fling. isFinished
+ * will be false if being flung
+ * .
+ */
+
+ if (!mScroller!!.isFinished)
+ abortAnimatedScroll()
+
+
+ // Remember where the motion event started
+ mLastMotionY = ev.y.toInt()
+ mActivePointerId = ev.getPointerId(0)
+ startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_TOUCH)
+ }
+
+ MotionEvent.ACTION_MOVE ->
+ {
+ val activePointerIndex = ev.findPointerIndex(mActivePointerId)
+
+ if (activePointerIndex == -1)
+ Log.e(TAG, "Invalid pointerId=$mActivePointerId in onTouchEvent")
+
+ val y = ev.getY(activePointerIndex).toInt()
+ var deltaY = mLastMotionY - y
+ if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset, ViewCompat.TYPE_TOUCH))
+ {
+ deltaY -= mScrollConsumed[1]
+ mNestedYOffset += mScrollOffset[1]
+ }
+ if (!mIsBeingDragged && abs(deltaY) > mTouchSlop)
+ {
+ val parent = parent
+ parent?.requestDisallowInterceptTouchEvent(true)
+ mIsBeingDragged = true
+
+ if (deltaY > 0)
+ deltaY -= mTouchSlop
+ else
+ deltaY += mTouchSlop
+ }
+ if (mIsBeingDragged)
+ {
+ // Scroll to follow the motion event
+ mLastMotionY = y - mScrollOffset[1]
+ val oldY = scrollY
+ val range = scrollRange
+ val overscrollMode = overScrollMode
+ val canOverscroll = (overscrollMode == OVER_SCROLL_ALWAYS || overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)
+
+ // Calling overScrollByCompat will call onOverScrolled, which
+ // calls onScrollChanged if applicable.
+
+ // Break our velocity if we hit a scroll barrier.
+ if (overScrollByCompat(deltaY, 0, scrollY, range, 0, 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH))
+ mVelocityTracker!!.clear()
+
+ val scrolledDeltaY = scrollY - oldY
+ val unconsumedY = deltaY - scrolledDeltaY
+ mScrollConsumed[1] = 0
+ dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset, ViewCompat.TYPE_TOUCH, mScrollConsumed)
+
+ mLastMotionY -= mScrollOffset[1]
+ mNestedYOffset += mScrollOffset[1]
+ if (canOverscroll)
+ {
+ deltaY -= mScrollConsumed[1]
+ ensureGlows()
+ val pulledToY = oldY + deltaY
+ if (pulledToY < 0)
+ {
+ EdgeEffectCompat.onPull(mEdgeGlowBottom!!, deltaY.toFloat() / height, ev.getX(activePointerIndex) / width)
+
+ if (!mEdgeGlowBottom!!.isFinished)
+ mEdgeGlowBottom!!.onRelease()
+
+ }
+ else if (pulledToY > range)
+ {
+ EdgeEffectCompat.onPull(mEdgeGlowBottom!!, deltaY.toFloat() / height, 1f - ev.getX(activePointerIndex) / width)
+
+ if (!mEdgeGlowTop!!.isFinished)
+ mEdgeGlowTop!!.onRelease()
+
+ }
+
+
+ if (mEdgeGlowTop != null && (!mEdgeGlowTop!!.isFinished || !mEdgeGlowBottom!!.isFinished))
+ ViewCompat.postInvalidateOnAnimation(this)
+ }
+ }
+ }
+ MotionEvent.ACTION_UP ->
+ {
+ val velocityTracker = mVelocityTracker
+ velocityTracker!!.computeCurrentVelocity(1000, mMaximumVelocity.toFloat())
+ val initialVelocity = velocityTracker.getYVelocity(mActivePointerId).toInt()
+ if (abs(initialVelocity) > mMinimumVelocity)
+ {
+ if (!dispatchNestedPreFling(0f, -initialVelocity.toFloat()))
+ {
+ dispatchNestedFling(0f, -initialVelocity.toFloat(), true)
+ fling(-initialVelocity)
+ }
+ }
+ else if (mScroller!!.springBack(scrollX, scrollY, 0, 0, 0, scrollRange))
+ ViewCompat.postInvalidateOnAnimation(this)
+
+ mActivePointerId = INVALID_POINTER
+ endDrag()
+ }
+ MotionEvent.ACTION_CANCEL ->
+ {
+ if (mIsBeingDragged && childCount > 0 && mScroller!!.springBack(scrollX, scrollY, 0, 0, 0, scrollRange))
+ ViewCompat.postInvalidateOnAnimation(this)
+
+ mActivePointerId = INVALID_POINTER
+ endDrag()
+ }
+ MotionEvent.ACTION_POINTER_DOWN ->
+ {
+ val index = ev.actionIndex
+ mLastMotionY = ev.getY(index).toInt()
+ mActivePointerId = ev.getPointerId(index)
+ }
+ MotionEvent.ACTION_POINTER_UP ->
+ {
+ onSecondaryPointerUp(ev)
+ mLastMotionY = ev.getY(ev.findPointerIndex(mActivePointerId)).toInt()
+ }
+ }
+
+
+ mVelocityTracker?.addMovement(vMotionEvent)
+ vMotionEvent.recycle()
+ return true
+ }
+
+ private fun onSecondaryPointerUp(ev: MotionEvent)
+ {
+ val pointerIndex = ev.actionIndex
+ val pointerId = ev.getPointerId(pointerIndex)
+ if (pointerId == mActivePointerId)
+ {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ // TODO: Make this decision more intelligent.
+ val newPointerIndex = if (pointerIndex == 0) 1 else 0
+ mLastMotionY = ev.getY(newPointerIndex).toInt()
+ mActivePointerId = ev.getPointerId(newPointerIndex)
+ mVelocityTracker?.clear()
+ }
+ }
+
+ override fun onGenericMotionEvent(event: MotionEvent): Boolean
+ {
+ if (event.source and InputDeviceCompat.SOURCE_CLASS_POINTER != 0)
+ {
+ if (event.action == MotionEvent.ACTION_SCROLL)
+ {
+ if (!mIsBeingDragged)
+ {
+ val vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
+ if (vScroll != 0f)
+ {
+ val delta = (vScroll * verticalScrollFactorCompat).toInt()
+ val range = scrollRange
+ val oldScrollY = scrollY
+ var newScrollY = oldScrollY - delta
+ if (newScrollY < 0)
+ newScrollY = 0
+ else if (newScrollY > range)
+ newScrollY = range
+ if (newScrollY != oldScrollY)
+ {
+ super.scrollTo(scrollX, newScrollY)
+ return true
+ }
+ }
+ }
+ }
+ }
+ return false
+ }
+
+ private val verticalScrollFactorCompat: Float
+ get()
+ {
+ if (mVerticalScrollFactor == 0f)
+ {
+ val outValue = TypedValue()
+ val context = context
+ if (!context.theme.resolveAttribute(android.R.attr.listPreferredItemHeight, outValue, true))
+ throw IllegalStateException("Expected theme to define listPreferredItemHeight.")
+
+ mVerticalScrollFactor = outValue.getDimension(context.resources.displayMetrics)
+ }
+ return mVerticalScrollFactor
+ }
+
+ override fun onOverScrolled(scrollX: Int, scrollY: Int, clampedX: Boolean, clampedY: Boolean)
+ = super.scrollTo(scrollX, scrollY)
+
+ @Suppress("MemberVisibilityCanBePrivate", "UNUSED_PARAMETER")
+ fun overScrollByCompat(deltaY: Int, scrollX: Int, scrollY: Int, scrollRangeY: Int, mX: Int, mY: Int, isTouchEvent: Boolean): Boolean
+ {
+ var maxOverScrollX = mX
+ var maxOverScrollY = mY
+ val overScrollMode = overScrollMode
+ val canScrollHorizontal = computeHorizontalScrollRange() > computeHorizontalScrollExtent()
+ val canScrollVertical = computeVerticalScrollRange() > computeVerticalScrollExtent()
+ val overScrollHorizontal = (overScrollMode == OVER_SCROLL_ALWAYS || overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal)
+ val overScrollVertical = (overScrollMode == OVER_SCROLL_ALWAYS || overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical)
+ var newScrollX = scrollX
+
+ if (!overScrollHorizontal)
+ maxOverScrollX = 0
+
+ var newScrollY = scrollY + deltaY
+
+ if (!overScrollVertical)
+ maxOverScrollY = 0
+
+
+ // Clamp values if at the limits and record
+ val left = -maxOverScrollX
+ val right = maxOverScrollX
+ val top = -maxOverScrollY
+ val bottom = maxOverScrollY + scrollRangeY
+ var clampedX = false
+ if (newScrollX > right)
+ {
+ newScrollX = right
+ clampedX = true
+ }
+ else if (newScrollX < left)
+ {
+ newScrollX = left
+ clampedX = true
+ }
+ var clampedY = false
+ if (newScrollY > bottom)
+ {
+ newScrollY = bottom
+ clampedY = true
+ }
+ else if (newScrollY < top)
+ {
+ newScrollY = top
+ clampedY = true
+ }
+ if (clampedY && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH) && !mIsBeingDragged)
+ mScroller!!.springBack(newScrollX, newScrollY, 0, 0, 0, scrollRange)
+
+ onOverScrolled(newScrollX, newScrollY, clampedX, clampedY)
+ return clampedX || clampedY
+ }
+
+ val scrollRange: Int
+ get()
+ {
+ var scrollRange = 0
+ if (childCount > 0)
+ {
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ val childSize = child.height + lp.topMargin + lp.bottomMargin
+ val parentSpace = height - paddingTop - paddingBottom
+ scrollRange = max(0, childSize - parentSpace)
+ }
+ return scrollRange
+ }
+
+ /**
+ *
+ *
+ * Finds the next focusable component that fits in the specified bounds.
+ *
+ *
+ * @param topFocus look for a candidate is the one at the top of the bounds
+ * if topFocus is true, or at the bottom of the bounds if topFocus is
+ * false
+ * @param top the top offset of the bounds in which a focusable must be
+ * found
+ * @param bottom the bottom offset of the bounds in which a focusable must
+ * be found
+ * @return the next focusable component in the bounds or null if none can
+ * be found
+ */
+ private fun findFocusableViewInBounds(topFocus: Boolean, top: Int, bottom: Int): View?
+ {
+ val focusable: List = getFocusables(FOCUS_FORWARD)
+ var focusCandidate: View? = null
+
+ /*
+ * A fully contained focusable is one where its top is below the bound's
+ * top, and its bottom is above the bound's bottom. A partially
+ * contained focusable is one where some part of it is within the
+ * bounds, but it also has some part that is not within bounds. A fully contained
+ * focusable is preferred to a partially contained focusable.
+ */
+ var foundFullyContainedFocusable = false
+ for (view: View in focusable)
+ {
+ val viewTop = view.top
+ val viewBottom = view.bottom
+ if (top < viewBottom && viewTop < bottom)
+ {
+ /*
+ * the focusable is in the target area, it is a candidate for
+ * focusing
+ */
+ val viewIsFullyContained = top < viewTop && viewBottom < bottom
+ if (focusCandidate == null)
+ {
+ /* No candidate, take this one */
+ focusCandidate = view
+ foundFullyContainedFocusable = viewIsFullyContained
+ }
+ else
+ {
+ val viewIsCloserToBoundary = (topFocus && viewTop < focusCandidate.top || !topFocus && viewBottom > focusCandidate.bottom)
+ if (foundFullyContainedFocusable)
+ {
+ if (viewIsFullyContained && viewIsCloserToBoundary)
+ focusCandidate = view
+ }
+ else
+ {
+ if (viewIsFullyContained)
+ {
+ /* Any fully contained view beats a partially contained view */
+ focusCandidate = view
+ foundFullyContainedFocusable = true
+ }
+ else if (viewIsCloserToBoundary)
+ focusCandidate = view
+ }
+ }
+ }
+ }
+ return focusCandidate
+ }
+
+ /**
+ *
+ * Handles scrolling in response to a "page up/down" shortcut press. This
+ * method will scroll the view by one page up or down and give the focus
+ * to the topmost/bottommost component in the new visible area. If no
+ * component is a good candidate for focus, this scrollview reclaims the
+ * focus.
+ *
+ * @param direction the scroll direction: [android.view.View.FOCUS_UP]
+ * to go one page up or
+ * [android.view.View.FOCUS_DOWN] to go one page down
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ fun pageScroll(direction: Int): Boolean
+ {
+ val down = direction == FOCUS_DOWN
+ val height = height
+ if (down)
+ {
+ mTempRect.top = scrollY + height
+ val count = childCount
+ if (count > 0)
+ {
+ val view = getChildAt(count - 1)
+ val lp = view.layoutParams as LayoutParams
+ val bottom = view.bottom + lp.bottomMargin + paddingBottom
+
+ if (mTempRect.top + height > bottom)
+ mTempRect.top = bottom - height
+
+ }
+ }
+ else
+ {
+ mTempRect.top = scrollY - height
+
+ if (mTempRect.top < 0)
+ mTempRect.top = 0
+
+ }
+ mTempRect.bottom = mTempRect.top + height
+ return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom)
+ }
+
+ /**
+ *
+ * Handles scrolling in response to a "home/end" shortcut press. This
+ * method will scroll the view to the top or bottom and give the focus
+ * to the topmost/bottommost component in the new visible area. If no
+ * component is a good candidate for focus, this scrollview reclaims the
+ * focus.
+ *
+ * @param direction the scroll direction: [android.view.View.FOCUS_UP]
+ * to go the top of the view or
+ * [android.view.View.FOCUS_DOWN] to go the bottom
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ fun fullScroll(direction: Int): Boolean
+ {
+ val down = direction == FOCUS_DOWN
+ val height = height
+ mTempRect.top = 0
+ mTempRect.bottom = height
+ if (down)
+ {
+ val count = childCount
+ if (count > 0)
+ {
+ val view = getChildAt(count - 1)
+ val lp = view.layoutParams as LayoutParams
+ mTempRect.bottom = view.bottom + lp.bottomMargin + paddingBottom
+ mTempRect.top = mTempRect.bottom - height
+ }
+ }
+ return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom)
+ }
+
+ /**
+ *
+ * Scrolls the view to make the area defined by `top` and
+ * `bottom` visible. This method attempts to give the focus
+ * to a component visible in this area. If no component can be focused in
+ * the new visible area, the focus is reclaimed by this ScrollView.
+ *
+ * @param direction the scroll direction: [android.view.View.FOCUS_UP]
+ * to go upward, [android.view.View.FOCUS_DOWN] to downward
+ * @param top the top offset of the new area to be made visible
+ * @param bottom the bottom offset of the new area to be made visible
+ * @return true if the key event is consumed by this method, false otherwise
+ */
+ private fun scrollAndFocus(direction: Int, top: Int, bottom: Int): Boolean
+ {
+ var handled = true
+ val height = height
+ val containerTop = scrollY
+ val containerBottom = containerTop + height
+ val up = direction == FOCUS_UP
+ var newFocused = findFocusableViewInBounds(up, top, bottom)
+
+ if (newFocused == null)
+ newFocused = this
+
+ if (top >= containerTop && bottom <= containerBottom)
+ handled = false
+ else
+ {
+ val delta =
+ if (up)
+ top - containerTop
+ else
+ bottom - containerBottom
+ doScrollY(delta)
+ }
+
+ if (newFocused !== findFocus()) newFocused.requestFocus(direction)
+ return handled
+ }
+
+ /**
+ * Handle scrolling in response to an up or down arrow click.
+ *
+ * @param direction The direction corresponding to the arrow key that was
+ * pressed
+ * @return True if we consumed the event, false otherwise
+ */
+ fun arrowScroll(direction: Int): Boolean
+ {
+ var currentFocused = findFocus()
+ if (currentFocused === this) currentFocused = null
+ val nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction)
+ val maxJump = maxScrollAmount
+ if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, height))
+ {
+ nextFocused.getDrawingRect(mTempRect)
+ offsetDescendantRectToMyCoords(nextFocused, mTempRect)
+ val scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect)
+ doScrollY(scrollDelta)
+ nextFocused.requestFocus(direction)
+ }
+ else
+ {
+ // no new focus
+ var scrollDelta = maxJump
+ if (direction == FOCUS_UP && scrollY < scrollDelta)
+ scrollDelta = scrollY
+ else if (direction == FOCUS_DOWN)
+ {
+ if (childCount > 0)
+ {
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ val daBottom = child.bottom + lp.bottomMargin
+ val screenBottom = scrollY + height - paddingBottom
+ scrollDelta = min(daBottom - screenBottom, maxJump)
+ }
+ }
+
+ if (scrollDelta == 0)
+ return false
+
+ doScrollY(if (direction == FOCUS_DOWN) scrollDelta else -scrollDelta)
+ }
+
+ if (currentFocused != null && currentFocused.isFocused && isOffScreen(currentFocused))
+ {
+ // previously focused item still has focus and is off screen, give
+ // it up (take it back to ourselves)
+ // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
+ // sure to
+ // get it)
+ val descendantFocusability = descendantFocusability // save
+ setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS)
+ requestFocus()
+ setDescendantFocusability(descendantFocusability) // restore
+ }
+
+ return true
+ }
+
+ /**
+ * @return whether the descendant of this scroll view is scrolled off
+ * screen.
+ */
+ private fun isOffScreen(descendant: View): Boolean = !isWithinDeltaOfScreen(descendant, 0, height)
+
+ /**
+ * @return whether the descendant of this scroll view is within delta
+ * pixels of being on the screen.
+ */
+ private fun isWithinDeltaOfScreen(descendant: View, delta: Int, height: Int): Boolean
+ {
+ descendant.getDrawingRect(mTempRect)
+ offsetDescendantRectToMyCoords(descendant, mTempRect)
+ return (mTempRect.bottom + delta >= scrollY && mTempRect.top - delta <= scrollY + height)
+ }
+
+ /**
+ * Smooth scroll by a Y delta
+ *
+ * @param delta the number of pixels to scroll by on the Y axis
+ */
+ private fun doScrollY(delta: Int)
+ {
+ if (delta != 0)
+ {
+ if (isSmoothScrollingEnabled)
+ smoothScrollBy(0, delta)
+ else
+ scrollBy(0, delta)
+ }
+ }
+
+ /**
+ * Like [View.scrollBy], but scroll smoothly instead of immediately.
+ *
+ * @param x the number of pixels to scroll by on the X axis
+ * @param y the number of pixels to scroll by on the Y axis
+ */
+ @Suppress("MemberVisibilityCanBePrivate", "MemberVisibilityCanBePrivate")
+ fun smoothScrollBy(x: Int, y: Int)
+ {
+ if (childCount != 0)
+ {
+ var dy = y
+ val duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll
+ if (duration > ANIMATED_SCROLL_GAP)
+ {
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ val childSize = child.height + lp.topMargin + lp.bottomMargin
+ val parentSpace = height - paddingTop - paddingBottom
+ val scrollY = scrollY
+ val maxY = max(0, childSize - parentSpace)
+ dy = max(0, min(scrollY + dy, maxY)) - scrollY
+ mScroller!!.startScroll(scrollX, scrollY, 0, dy)
+ runAnimatedScroll(false)
+ }
+ else
+ {
+ if (!mScroller!!.isFinished)
+ abortAnimatedScroll()
+
+ scrollBy(x, dy)
+ }
+
+ mLastScroll = AnimationUtils.currentAnimationTimeMillis()
+ }
+ }
+
+ /**
+ * Like [.scrollTo], but scroll smoothly instead of immediately.
+ *
+ * @param x the position where to scroll on the X axis
+ * @param y the position where to scroll on the Y axis
+ */
+ fun smoothScrollTo(x: Int, y: Int) = smoothScrollBy(x - scrollX, y - scrollY)
+
+ /**
+ *
+ * The scroll range of a scroll view is the overall height of all of its
+ * children.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ override fun computeVerticalScrollRange(): Int
+ {
+ val count = childCount
+ val parentSpace = height - paddingBottom - paddingTop
+
+ if (count == 0)
+ return parentSpace
+
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ var scrollRange = child.bottom + lp.bottomMargin
+ val scrollY = scrollY
+ val overscrollBottom = max(0, scrollRange - parentSpace)
+
+ if (scrollY < 0)
+ scrollRange -= scrollY
+ else if (scrollY > overscrollBottom)
+ scrollRange += scrollY - overscrollBottom
+
+ return scrollRange
+ }
+
+ /** @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ override fun computeVerticalScrollOffset(): Int = max(0, super.computeVerticalScrollOffset())
+
+ /** @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ override fun computeVerticalScrollExtent(): Int = super.computeVerticalScrollExtent()
+
+ /** @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ override fun computeHorizontalScrollRange(): Int = super.computeHorizontalScrollRange()
+
+ /** @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ override fun computeHorizontalScrollOffset(): Int = super.computeHorizontalScrollOffset()
+
+ /** @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ override fun computeHorizontalScrollExtent(): Int = super.computeHorizontalScrollExtent()
+
+
+ override fun measureChild(child: View, parentWidthMeasureSpec: Int, parentHeightMeasureSpec: Int)
+ {
+ val lp = child.layoutParams
+ val childWidthMeasureSpec: Int = getChildMeasureSpec(parentWidthMeasureSpec, paddingLeft + paddingRight, lp.width)
+ val childHeightMeasureSpec: Int = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
+ }
+
+ override fun measureChildWithMargins(child: View, parentWidthMeasureSpec: Int, widthUsed: Int, parentHeightMeasureSpec: Int, heightUsed: Int)
+ {
+ val lp = child.layoutParams as MarginLayoutParams
+
+ val childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, paddingLeft + paddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width)
+
+ val childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED)
+
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
+ }
+
+ override fun computeScroll()
+ {
+ if (!mScroller!!.isFinished)
+ {
+ mScroller!!.computeScrollOffset()
+ val y = mScroller!!.currY
+ var unconsumed = y - scrollY
+
+ // Nested Scrolling Pre Pass
+ mScrollConsumed[1] = 0
+ dispatchNestedPreScroll(0, unconsumed, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)
+ unconsumed -= mScrollConsumed[1]
+ val range = scrollRange
+
+ if (unconsumed != 0)
+ {
+ // Internal Scroll
+ val oldScrollY = scrollY
+ overScrollByCompat(unconsumed, scrollX, oldScrollY, range, 0, 0, false)
+ val scrolledByMe = scrollY - oldScrollY
+ unconsumed -= scrolledByMe
+
+ // Nested Scrolling Post Pass
+ mScrollConsumed[1] = 0
+ dispatchNestedScroll(0, scrolledByMe, 0, unconsumed, mScrollOffset, ViewCompat.TYPE_NON_TOUCH, mScrollConsumed)
+
+ unconsumed -= mScrollConsumed[1]
+ }
+ if (unconsumed != 0)
+ {
+ val mode = overScrollMode
+ val canOverscroll = (mode == OVER_SCROLL_ALWAYS || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0))
+ if (canOverscroll)
+ {
+ ensureGlows()
+
+ if (unconsumed < 0 && mEdgeGlowTop!!.isFinished)
+ mEdgeGlowTop!!.onAbsorb(mScroller!!.currVelocity.toInt())
+
+ else if (mEdgeGlowBottom!!.isFinished)
+ mEdgeGlowBottom!!.onAbsorb(mScroller!!.currVelocity.toInt())
+
+ }
+
+ abortAnimatedScroll()
+ }
+
+ if (!mScroller!!.isFinished)
+ ViewCompat.postInvalidateOnAnimation(this)
+
+ }
+ }
+
+ private fun runAnimatedScroll(participateInNestedScrolling: Boolean)
+ {
+ if (participateInNestedScrolling)
+ startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL, ViewCompat.TYPE_NON_TOUCH)
+ else
+ stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)
+
+ ViewCompat.postInvalidateOnAnimation(this)
+ }
+
+ private fun abortAnimatedScroll()
+ {
+ mScroller!!.abortAnimation()
+ stopNestedScroll(ViewCompat.TYPE_NON_TOUCH)
+ }
+
+ /**
+ * Scrolls the view to the given child.
+ *
+ * @param child the View to scroll to
+ */
+ private fun scrollToChild(child: View)
+ {
+ child.getDrawingRect(mTempRect)
+
+ /* Offset from child's local coordinates to ScrollView coordinates */
+ offsetDescendantRectToMyCoords(child, mTempRect)
+
+ val scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect)
+
+ if (scrollDelta != 0)
+ scrollBy(0, scrollDelta)
+
+ }
+
+ /**
+ * If rect is off screen, scroll just enough to get it (or at least the
+ * first screen size chunk of it) on screen.
+ *
+ * @param rect The rectangle.
+ * @param immediate True to scroll immediately without animation
+ * @return true if scrolling was performed
+ */
+ private fun scrollToChildRect(rect: Rect, immediate: Boolean): Boolean
+ {
+ val delta = computeScrollDeltaToGetChildRectOnScreen(rect)
+ val scroll = delta != 0
+ if (scroll)
+ if (immediate)
+ scrollBy(0, delta)
+ else
+ smoothScrollBy(0, delta)
+
+ return scroll
+ }
+
+ /**
+ * Compute the amount to scroll in the Y direction in order to get
+ * a rectangle completely on the screen (or, if taller than the screen,
+ * at least the first screen size chunk of it).
+ *
+ * @param rect The rect.
+ * @return The scroll delta.
+ */
+ private fun computeScrollDeltaToGetChildRectOnScreen(rect: Rect): Int
+ {
+ if (childCount == 0) return 0
+ val height = height
+ var screenTop = scrollY
+ var screenBottom = screenTop + height
+ val actualScreenBottom = screenBottom
+ val fadingEdge = verticalFadingEdgeLength
+
+ // TODO: screenTop should be incremented by fadingEdge * getTopFadingEdgeStrength (but for
+ // the target scroll distance).
+ // leave room for top fading edge as long as rect isn't at very top
+ if (rect.top > 0)
+ screenTop += fadingEdge
+
+ // TODO: screenBottom should be decremented by fadingEdge * getBottomFadingEdgeStrength (but
+ // for the target scroll distance).
+ // leave room for bottom fading edge as long as rect isn't at very bottom
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ if (rect.bottom < child.height + lp.topMargin + lp.bottomMargin)
+ screenBottom -= fadingEdge
+
+ var scrollYDelta = 0
+ if (rect.bottom > screenBottom && rect.top > screenTop)
+ {
+
+ // need to move down to get it in view: move down just enough so
+ // that the entire rectangle is in view (or at least the first
+ // screen size chunk).
+
+ scrollYDelta += if (rect.height() > height) // just enough to get screen size chunk on
+ (rect.top - screenTop)
+ else // get entire rect at bottom of screen
+ (rect.bottom - screenBottom)
+
+ // make sure we aren't scrolling beyond the end of our content
+ val bottom = child.bottom + lp.bottomMargin
+ val distanceToBottom = bottom - actualScreenBottom
+ scrollYDelta = min(scrollYDelta, distanceToBottom)
+ }
+ else if (rect.top < screenTop && rect.bottom < screenBottom)
+ {
+ // need to move up to get it in view: move up just enough so that
+ // entire rectangle is in view (or at least the first screen
+ // size chunk of it).
+
+ scrollYDelta -= if (rect.height() > height) // screen size chunk
+ (screenBottom - rect.bottom)
+ else // entire rect at top
+ (screenTop - rect.top)
+
+ // make sure we aren't scrolling any further than the top our content
+ scrollYDelta = max(scrollYDelta, -scrollY)
+ }
+ return scrollYDelta
+ }
+
+ override fun requestChildFocus(child: View, focused: View)
+ {
+ if (!mIsLayoutDirty)
+ scrollToChild(focused)
+ else // The child may not be laid out yet, we can't compute the scroll yet
+ mChildToScrollTo = focused
+
+ super.requestChildFocus(child, focused)
+ }
+
+
+ override fun onRequestFocusInDescendants(direction: Int, previouslyFocusedRect: Rect): Boolean
+ {
+ // convert from forward / backward notation to up / down / left / right
+ // (ugh).
+ var mDirection = direction
+
+ if (mDirection == FOCUS_FORWARD)
+ mDirection = FOCUS_DOWN
+ else if (direction == FOCUS_BACKWARD)
+ mDirection = FOCUS_UP
+
+ val nextFocus = FocusFinder.getInstance().findNextFocusFromRect(this, previouslyFocusedRect, mDirection) ?: return false
+ return if (isOffScreen(nextFocus)) false
+ else nextFocus.requestFocus(mDirection, previouslyFocusedRect)
+ }
+
+ override fun requestChildRectangleOnScreen(child: View, rectangle: Rect, immediate: Boolean): Boolean
+ {
+ // offset into coordinate space of this scroll view
+ rectangle.offset(child.left - child.scrollX, child.top - child.scrollY)
+ return scrollToChildRect(rectangle, immediate)
+ }
+
+ override fun requestLayout()
+ {
+ mIsLayoutDirty = true
+ super.requestLayout()
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int)
+ {
+ super.onLayout(changed, l, t, r, b)
+ mIsLayoutDirty = false
+ // Give a child focus if it needs it
+ if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo!!, this))
+ scrollToChild(mChildToScrollTo!!)
+
+ mChildToScrollTo = null
+ if (!mIsLaidOut)
+ {
+ // If there is a saved state, scroll to the position saved in that state.
+ if (mSavedState != null)
+ {
+ scrollTo(scrollX, mSavedState!!.scrollPosition)
+ mSavedState = null
+ } // mScrollY default value is "0"
+
+ // Make sure current scrollY position falls into the scroll range. If it doesn't,
+ // scroll such that it does.
+ var childSize = 0
+ if (childCount > 0)
+ {
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ childSize = child.measuredHeight + lp.topMargin + lp.bottomMargin
+ }
+ val parentSpace = b - t - paddingTop - paddingBottom
+ val currentScrollY = scrollY
+ val newScrollY = clamp(currentScrollY, parentSpace, childSize)
+
+ if (newScrollY != currentScrollY)
+ scrollTo(scrollX, newScrollY)
+ }
+
+ // Calling this with the present values causes it to re-claim them
+ scrollTo(scrollX, scrollY)
+ mIsLaidOut = true
+ }
+
+ public override fun onAttachedToWindow()
+ {
+ super.onAttachedToWindow()
+ mIsLaidOut = false
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int)
+ {
+ super.onSizeChanged(w, h, oldw, oldh)
+ val currentFocused = findFocus()
+
+ if (currentFocused != null && this !== currentFocused)
+ {
+ // If the currently-focused view was visible on the screen when the
+ // screen was at the old height, then scroll the screen to make that
+ // view visible with the new screen height.
+ if (isWithinDeltaOfScreen(currentFocused, 0, oldh))
+ {
+ currentFocused.getDrawingRect(mTempRect)
+ offsetDescendantRectToMyCoords(currentFocused, mTempRect)
+ val scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect)
+ doScrollY(scrollDelta)
+ }
+ }
+ }
+
+ /**
+ * Fling the scroll view
+ *
+ * @param velocityY The initial velocity in the Y direction. Positive
+ * numbers mean that the finger/cursor is moving down the screen,
+ * which means we want to scroll towards the top.
+ */
+ @Suppress("MemberVisibilityCanBePrivate")
+ fun fling(velocityY: Int)
+ {
+ if (childCount > 0)
+ {
+ // overscroll
+ mScroller!!.fling(scrollX, scrollY, 0, velocityY, 0, 0, Int.MIN_VALUE, Int.MAX_VALUE, 0, 0)
+ runAnimatedScroll(true)
+ }
+ }
+
+ private fun endDrag()
+ {
+ mIsBeingDragged = false
+ recycleVelocityTracker()
+ stopNestedScroll(ViewCompat.TYPE_TOUCH)
+
+ mEdgeGlowTop?.onRelease()
+ mEdgeGlowBottom?.onRelease()
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
+ * This version also clamps the scrolling to the bounds of our child.
+ */
+ override fun scrollTo(a: Int, b: Int)
+ {
+ // we rely on the fact the View.scrollBy calls scrollTo.
+ var x = a
+ var y = b
+ if (childCount > 0)
+ {
+ val child = getChildAt(0)
+ val lp = child.layoutParams as LayoutParams
+ val parentSpaceHorizontal = width - paddingLeft - paddingRight
+ val childSizeHorizontal = child.width + lp.leftMargin + lp.rightMargin
+ val parentSpaceVertical = height - paddingTop - paddingBottom
+ val childSizeVertical = child.height + lp.topMargin + lp.bottomMargin
+ x = clamp(x, parentSpaceHorizontal, childSizeHorizontal)
+ y = clamp(y, parentSpaceVertical, childSizeVertical)
+
+ if (x != scrollX || y != scrollY)
+ super.scrollTo(x, y)
+
+ }
+ }
+
+ private fun ensureGlows()
+ {
+ if (overScrollMode != OVER_SCROLL_NEVER)
+ {
+ if (mEdgeGlowTop == null)
+ {
+ val context = context
+ mEdgeGlowTop = BouncyEdgeEffect(context, spring, this, EdgeEffectFactory.DIRECTION_TOP, flingAnimationSize, overscrollAnimationSize)
+ mEdgeGlowBottom = BouncyEdgeEffect(context, spring, this, EdgeEffectFactory.DIRECTION_BOTTOM, flingAnimationSize, overscrollAnimationSize)
+ }
+ }
+ else
+ {
+ mEdgeGlowTop = null
+ mEdgeGlowBottom = null
+ }
+ }
+
+ override fun draw(canvas: Canvas)
+ {
+ super.draw(canvas)
+ if (mEdgeGlowTop != null)
+ {
+ val scrollY = scrollY
+ if (!mEdgeGlowTop!!.isFinished)
+ {
+ val restoreCount = canvas.save()
+ var width = width
+ var height = height
+ var xTranslation = 0
+ var yTranslation = min(0, scrollY)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || clipToPadding)
+ {
+ width -= paddingLeft + paddingRight
+ xTranslation += paddingLeft
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && clipToPadding)
+ {
+ height -= paddingTop + paddingBottom
+ yTranslation += paddingTop
+ }
+ canvas.translate(xTranslation.toFloat(), yTranslation.toFloat())
+
+ mEdgeGlowTop!!.setSize(width, height)
+
+ if (mEdgeGlowTop!!.draw(canvas))
+ ViewCompat.postInvalidateOnAnimation(this)
+
+ canvas.restoreToCount(restoreCount)
+ }
+ if (!mEdgeGlowBottom!!.isFinished)
+ {
+ val restoreCount = canvas.save()
+ var width = width
+ var height = height
+ var xTranslation = 0
+ var yTranslation = max(scrollRange, scrollY) + height
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || clipToPadding)
+ {
+ width -= paddingLeft + paddingRight
+ xTranslation += paddingLeft
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && clipToPadding)
+ {
+ height -= paddingTop + paddingBottom
+ yTranslation -= paddingBottom
+ }
+ canvas.translate((xTranslation - width).toFloat(), yTranslation.toFloat())
+ canvas.rotate(180f, width.toFloat(), 0f)
+ mEdgeGlowBottom!!.setSize(width, height)
+
+ if (mEdgeGlowBottom!!.draw(canvas))
+ ViewCompat.postInvalidateOnAnimation(this)
+
+ canvas.restoreToCount(restoreCount)
+ }
+ }
+ }
+
+ override fun onRestoreInstanceState(state: Parcelable)
+ {
+ if (state !is SavedState)
+ {
+ super.onRestoreInstanceState(state)
+ return
+ }
+ super.onRestoreInstanceState(state.superState)
+ mSavedState = state
+ requestLayout()
+ }
+
+ override fun onSaveInstanceState(): Parcelable
+ {
+ val superState = super.onSaveInstanceState()
+ val ss = SavedState(superState)
+ ss.scrollPosition = scrollY
+ return ss
+ }
+
+ internal class SavedState : BaseSavedState
+ {
+ var scrollPosition = 0
+
+ constructor(superState: Parcelable?) : super(superState)
+
+ @Suppress("unused")
+ constructor(source: Parcel) : super(source)
+ {
+ scrollPosition = source.readInt()
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int)
+ {
+ super.writeToParcel(dest, flags)
+ dest.writeInt(scrollPosition)
+ }
+
+ override fun toString(): String
+ = ("HorizontalScrollView.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " scrollPosition=" + scrollPosition + "}")
+
+
+ @Suppress("unused")
+ val creator: Creator = object : Creator
+ {
+ override fun createFromParcel(`in`: Parcel): SavedState = SavedState(`in`)
+
+ override fun newArray(size: Int): Array = arrayOfNulls(size)
+ }
+ }
+
+ internal class AccessibilityDelegate : AccessibilityDelegateCompat()
+ {
+ override fun performAccessibilityAction(host: View, action: Int, arguments: Bundle): Boolean
+ {
+ if (super.performAccessibilityAction(host, action, arguments))
+ return true
+
+
+ val nsvHost = host as BouncyNestedScrollView
+
+ if (!nsvHost.isEnabled)
+ return false
+
+ when (action)
+ {
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD ->
+ {
+ run{
+ val viewportHeight: Int = (nsvHost.height - nsvHost.paddingBottom - nsvHost.paddingTop)
+
+ val targetScrollY: Int = min(nsvHost.scrollY + viewportHeight, nsvHost.scrollRange)
+
+ if (targetScrollY != nsvHost.scrollY)
+ {
+ nsvHost.smoothScrollTo(0, targetScrollY)
+ return true
+ }
+ }
+
+ return false
+ }
+ AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD ->
+ {
+ run {
+ val viewportHeight: Int = (nsvHost.height - nsvHost.paddingBottom - nsvHost.paddingTop)
+ val targetScrollY: Int = max(nsvHost.scrollY - viewportHeight, 0)
+ if (targetScrollY != nsvHost.scrollY)
+ {
+ nsvHost.smoothScrollTo(0, targetScrollY)
+ return true
+ }
+ }
+ return false
+ }
+ }
+ return false
+ }
+
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat)
+ {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ val nsvHost = host as BouncyNestedScrollView
+ info.className = ScrollView::class.java.name
+ if (nsvHost.isEnabled)
+ {
+ val scrollRange = nsvHost.scrollRange
+ if (scrollRange > 0)
+ {
+ info.isScrollable = true
+ if (nsvHost.scrollY > 0)
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD)
+
+ if (nsvHost.scrollY < scrollRange)
+ info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD)
+
+ }
+ }
+ }
+
+ override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent)
+ {
+ super.onInitializeAccessibilityEvent(host, event)
+ val nsvHost = host as BouncyNestedScrollView
+ event.className = ScrollView::class.java.name
+ val scrollable = nsvHost.scrollRange > 0
+ event.isScrollable = scrollable
+ event.scrollX = nsvHost.scrollX
+ event.scrollY = nsvHost.scrollY
+ AccessibilityRecordCompat.setMaxScrollX(event, nsvHost.scrollX)
+ AccessibilityRecordCompat.setMaxScrollY(event, nsvHost.scrollRange)
+ }
+ }
+
+ companion object
+ {
+ private const val ANIMATED_SCROLL_GAP = 250
+ private const val MAX_SCROLL_FACTOR = 0.5f
+ private const val TAG = "BouncyNestedScrollView"
+
+ /**
+ * Sentinel value for no current active pointer.
+ * Used by [.mActivePointerId].
+ */
+ private const val INVALID_POINTER = -1
+ private val ACCESSIBILITY_DELEGATE = AccessibilityDelegate()
+ private val SCROLLVIEW_STYLEABLE = intArrayOf(android.R.attr.fillViewport)
+
+ /**
+ * Return true if child is a descendant of parent, (or equal to the parent).
+ */
+ private fun isViewDescendantOf(child: View, parent: View): Boolean
+ {
+ return when
+ {
+ child === parent -> true
+ else ->
+ {
+ val theParent = child.parent
+ (theParent is ViewGroup) && isViewDescendantOf(theParent as View, parent)
+ }
+ }
+ }
+
+ private fun clamp(n: Int, my: Int, child: Int): Int
+ {
+ return if (my >= child || n < 0) 0
+ else if ((my + n) > child) child - my
+ else n
+ }
+ }
+
+ init
+ {
+ initScrollView()
+
+ //read attributes
+ context.obtainStyledAttributes(attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0)
+ .apply {
+ isFillViewport = getBoolean(0, false)
+ recycle()
+ }
+ context.theme.obtainStyledAttributes(attrs, R.styleable.BouncyNestedScrollView, 0, 0)
+ .apply{
+ overscrollAnimationSize = getFloat(R.styleable.BouncyNestedScrollView_overscroll_bounce_animation_size, 0.5f)
+ flingAnimationSize = getFloat(R.styleable.BouncyNestedScrollView_fling_bounce_animation_size, 0.5f)
+ recycle()
+ }
+
+ mParentHelper = NestedScrollingParentHelper(this)
+ mChildHelper = NestedScrollingChildHelper(this)
+
+ // ...because why else would you be using this widget?
+ isNestedScrollingEnabled = true
+ ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE)
+ }
+
+
+}
\ No newline at end of file
diff --git a/bouncy/src/main/java/com/factor/bouncy/BouncyRecyclerView.kt b/bouncy/src/main/java/com/factor/bouncy/BouncyRecyclerView.kt
new file mode 100644
index 0000000..7c6b6bb
--- /dev/null
+++ b/bouncy/src/main/java/com/factor/bouncy/BouncyRecyclerView.kt
@@ -0,0 +1,151 @@
+package com.factor.bouncy
+
+import android.content.Context
+import android.graphics.Canvas
+import android.util.AttributeSet
+import android.widget.EdgeEffect
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+import com.factor.bouncy.util.BouncyViewHolder
+import com.factor.bouncy.util.DragDropAdapter
+import com.factor.bouncy.util.DragDropCallBack
+import com.factor.bouncy.util.OnOverPullListener
+
+
+@Suppress("unused")
+class BouncyRecyclerView(context: Context, attrs: AttributeSet?) : RecyclerView(context, attrs)
+{
+ abstract class Adapter: RecyclerView.Adapter(), DragDropAdapter
+
+ private lateinit var callBack: DragDropCallBack
+
+ var onOverPullListener: OnOverPullListener? = null
+
+ var overscrollAnimationSize = 0.5f
+
+ var flingAnimationSize = 0.5f
+
+ @Suppress("MemberVisibilityCanBePrivate")
+ var longPressDragEnabled = false
+ set(value)
+ {
+ field = value
+ if (adapter is DragDropAdapter) callBack.setDragEnabled(value)
+ }
+
+ @Suppress("MemberVisibilityCanBePrivate")
+ var itemSwipeEnabled = false
+ set(value)
+ {
+ field = value
+ if (adapter is DragDropAdapter) callBack.setSwipeEnabled(value)
+ }
+
+ val spring: SpringAnimation = SpringAnimation(this, SpringAnimation.TRANSLATION_Y)
+ .setSpring(
+ SpringForce()
+ .setFinalPosition(0f)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
+ .setStiffness(SpringForce.STIFFNESS_LOW)
+ )
+
+
+ override fun setAdapter(adapter: RecyclerView.Adapter<*>?)
+ {
+ super.setAdapter(adapter)
+
+ if (adapter is DragDropAdapter)
+ {
+ callBack = DragDropCallBack(adapter, longPressDragEnabled, itemSwipeEnabled)
+ val touchHelper = ItemTouchHelper(callBack)
+ touchHelper.attachToRecyclerView(this)
+ }
+ }
+
+
+ inline fun RecyclerView.forEachVisibleHolder(action: (T) -> Unit)
+ {
+ for (i in 0 until childCount) action(getChildViewHolder(getChildAt(i)) as T)
+ }
+
+ init
+ {
+ //read attributes
+ context.theme.obtainStyledAttributes(attrs, R.styleable.BouncyRecyclerView, 0, 0)
+ .apply{
+ overscrollAnimationSize = getFloat(R.styleable.BouncyNestedScrollView_overscroll_bounce_animation_size, 0.5f)
+ flingAnimationSize = getFloat(R.styleable.BouncyNestedScrollView_fling_bounce_animation_size, 0.5f)
+ longPressDragEnabled = getBoolean(R.styleable.BouncyRecyclerView_allow_drag_reorder, false)
+ itemSwipeEnabled = getBoolean(R.styleable.BouncyRecyclerView_allow_item_swipe, false)
+ recycle()
+ }
+
+ val rc = this
+
+ //create edge effect
+ this.edgeEffectFactory = object : RecyclerView.EdgeEffectFactory()
+ {
+ override fun createEdgeEffect(recyclerView: RecyclerView, direction: Int): EdgeEffect
+ {
+ return object : EdgeEffect(recyclerView.context)
+ {
+ override fun onPull(deltaDistance: Float)
+ {
+ super.onPull(deltaDistance)
+ onPullAnimation(deltaDistance)
+ }
+
+ override fun onPull(deltaDistance: Float, displacement: Float)
+ {
+ super.onPull(deltaDistance, displacement)
+ onPullAnimation(deltaDistance)
+ if (direction == DIRECTION_BOTTOM)
+ onOverPullListener?.onOverPulledBottom(deltaDistance)
+ else if (direction == DIRECTION_TOP)
+ onOverPullListener?.onOverPulledTop(deltaDistance)
+ }
+
+ private fun onPullAnimation(deltaDistance: Float)
+ {
+ val delta =
+ if (direction == DIRECTION_BOTTOM)
+ -1 * recyclerView.width * deltaDistance * overscrollAnimationSize
+ else 1 * recyclerView.width * deltaDistance * overscrollAnimationSize
+ spring.cancel()
+ rc.translationY += delta
+
+ forEachVisibleHolder{holder: ViewHolder? -> if (holder is BouncyViewHolder)holder.onPulled(deltaDistance)}
+ }
+
+ override fun onRelease()
+ {
+ super.onRelease()
+ onOverPullListener?.onRelease()
+ spring.start()
+
+ forEachVisibleHolder{holder: ViewHolder? -> if (holder is BouncyViewHolder)holder.onRelease()}
+ }
+
+ override fun onAbsorb(velocity: Int)
+ {
+ super.onAbsorb(velocity)
+ val v =
+ if (direction == DIRECTION_BOTTOM) -1 * velocity * flingAnimationSize
+ else 1 * velocity * flingAnimationSize
+ spring.setStartVelocity(v).start()
+
+ forEachVisibleHolder{holder: ViewHolder? -> if (holder is BouncyViewHolder)holder.onAbsorb(velocity)}
+ }
+
+ override fun draw(canvas: Canvas?): Boolean
+ {
+ setSize(0, 0)
+ return super.draw(canvas)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/bouncy/src/main/java/com/factor/bouncy/util/BouncyEdgeEffect.kt b/bouncy/src/main/java/com/factor/bouncy/util/BouncyEdgeEffect.kt
new file mode 100644
index 0000000..fa6179c
--- /dev/null
+++ b/bouncy/src/main/java/com/factor/bouncy/util/BouncyEdgeEffect.kt
@@ -0,0 +1,62 @@
+package com.factor.bouncy.util
+
+import android.content.Context
+import android.graphics.Canvas
+import android.view.View
+import android.widget.EdgeEffect
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.recyclerview.widget.RecyclerView.EdgeEffectFactory.DIRECTION_BOTTOM
+
+class BouncyEdgeEffect(context: Context?,
+ private val spring : SpringAnimation,
+ private val view : View,
+ private val direction : Int,
+ private val flingSize : Float,
+ private val overscrollSize : Float) : EdgeEffect(context)
+{
+
+ override fun onPull(deltaDistance: Float)
+ {
+ super.onPull(deltaDistance)
+ onPullAnimation(deltaDistance)
+ }
+
+ override fun onPull(deltaDistance: Float, displacement: Float)
+ {
+ super.onPull(deltaDistance, displacement)
+ onPullAnimation(deltaDistance)
+ }
+
+ private fun onPullAnimation(deltaDistance: Float)
+ {
+ val delta =
+ if (direction == DIRECTION_BOTTOM)
+ -1 * view.width * deltaDistance * overscrollSize
+ else 1 * view.width * deltaDistance * overscrollSize
+ spring.cancel()
+ view.translationY += delta
+
+ }
+
+ override fun onRelease()
+ {
+ super.onRelease()
+ spring.start()
+
+ }
+
+ override fun onAbsorb(velocity: Int)
+ {
+ super.onAbsorb(velocity)
+ val v: Float =
+ if (direction == DIRECTION_BOTTOM) -1 * velocity * flingSize
+ else 1 * velocity * flingSize
+ spring.setStartVelocity(v).start()
+ }
+
+ override fun draw(canvas: Canvas?): Boolean
+ {
+ setSize(0, 0)
+ return super.draw(canvas)
+ }
+}
\ No newline at end of file
diff --git a/bouncy/src/main/java/com/factor/bouncy/util/BouncyViewHolder.kt b/bouncy/src/main/java/com/factor/bouncy/util/BouncyViewHolder.kt
new file mode 100644
index 0000000..08d9200
--- /dev/null
+++ b/bouncy/src/main/java/com/factor/bouncy/util/BouncyViewHolder.kt
@@ -0,0 +1,13 @@
+package com.factor.bouncy.util
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+abstract class BouncyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
+{
+ abstract fun onPulled(delta: Float)
+
+ abstract fun onRelease()
+
+ abstract fun onAbsorb(velocity: Int)
+}
\ No newline at end of file
diff --git a/bouncy/src/main/java/com/factor/bouncy/util/DragDropAdapter.kt b/bouncy/src/main/java/com/factor/bouncy/util/DragDropAdapter.kt
new file mode 100644
index 0000000..beebc13
--- /dev/null
+++ b/bouncy/src/main/java/com/factor/bouncy/util/DragDropAdapter.kt
@@ -0,0 +1,12 @@
+package com.factor.bouncy.util
+
+import androidx.recyclerview.widget.RecyclerView
+
+interface DragDropAdapter
+{
+ fun onItemMoved(fromPosition: Int, toPosition: Int)
+ fun onItemSwipedToStart(viewHolder: RecyclerView.ViewHolder?, positionOfItem: Int)
+ fun onItemSwipedToEnd(viewHolder: RecyclerView.ViewHolder?, positionOfItem: Int)
+ fun onItemSelected(viewHolder: RecyclerView.ViewHolder?)
+ fun onItemReleased(viewHolder: RecyclerView.ViewHolder?)
+}
\ No newline at end of file
diff --git a/bouncy/src/main/java/com/factor/bouncy/util/DragDropCallBack.kt b/bouncy/src/main/java/com/factor/bouncy/util/DragDropCallBack.kt
new file mode 100644
index 0000000..ed11683
--- /dev/null
+++ b/bouncy/src/main/java/com/factor/bouncy/util/DragDropCallBack.kt
@@ -0,0 +1,78 @@
+package com.factor.bouncy.util
+
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.RecyclerView
+
+
+class DragDropCallBack(private val adapter: RecyclerView.Adapter<*>,
+ private var longPressDragEnabled: Boolean,
+ private var itemSwipeEnabled: Boolean) : ItemTouchHelper.Callback()
+{
+
+
+ override fun isLongPressDragEnabled(): Boolean
+ {
+ return longPressDragEnabled
+ }
+
+ override fun isItemViewSwipeEnabled(): Boolean
+ {
+ return itemSwipeEnabled
+ }
+
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, i: Int)
+ {
+ if (isItemViewSwipeEnabled && adapter is DragDropAdapter)
+ {
+ if (i == ItemTouchHelper.START)
+ adapter.onItemSwipedToStart(viewHolder, viewHolder.adapterPosition)
+ else if (i == ItemTouchHelper.END)
+ adapter.onItemSwipedToEnd(viewHolder, viewHolder.adapterPosition)
+ }
+ }
+
+ override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int
+ {
+ val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
+ val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
+ return makeMovementFlags(dragFlags, swipeFlags)
+ }
+
+ override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean
+ {
+ return if (isLongPressDragEnabled && adapter is DragDropAdapter)
+ {
+ adapter.onItemMoved(viewHolder.adapterPosition, target.adapterPosition)
+ true
+ }
+ else
+ false
+ }
+
+ override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int)
+ {
+ if (actionState != ItemTouchHelper.ACTION_STATE_IDLE && adapter is DragDropAdapter)
+ {
+ adapter.onItemSelected(viewHolder)
+ }
+ super.onSelectedChanged(viewHolder, actionState)
+ }
+
+ override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder)
+ {
+ super.clearView(recyclerView, viewHolder)
+ if (adapter is DragDropAdapter)
+ adapter.onItemReleased(viewHolder)
+ }
+
+ fun setDragEnabled(enabled: Boolean)
+ {
+ longPressDragEnabled = enabled
+ }
+
+ fun setSwipeEnabled(enabled: Boolean)
+ {
+ itemSwipeEnabled = enabled
+ }
+
+}
\ No newline at end of file
diff --git a/bouncy/src/main/java/com/factor/bouncy/util/OnOverPullListener.kt b/bouncy/src/main/java/com/factor/bouncy/util/OnOverPullListener.kt
new file mode 100644
index 0000000..77bf7d0
--- /dev/null
+++ b/bouncy/src/main/java/com/factor/bouncy/util/OnOverPullListener.kt
@@ -0,0 +1,10 @@
+package com.factor.bouncy.util
+
+interface OnOverPullListener
+{
+ fun onOverPulledTop(deltaDistance: Float)
+
+ fun onOverPulledBottom(deltaDistance: Float)
+
+ fun onRelease()
+}
\ No newline at end of file
diff --git a/bouncy/src/main/res/values/attrs.xml b/bouncy/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..ab706f3
--- /dev/null
+++ b/bouncy/src/main/res/values/attrs.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bouncy/src/test/java/com/factor/bouncy/ExampleUnitTest.kt b/bouncy/src/test/java/com/factor/bouncy/ExampleUnitTest.kt
new file mode 100644
index 0000000..dcb8805
--- /dev/null
+++ b/bouncy/src/test/java/com/factor/bouncy/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.factor.bouncy
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..0968a8c
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,26 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ ext.kotlin_version = "1.4.21"
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.0.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/example/.gitignore b/example/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/example/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/example/build.gradle b/example/build.gradle
new file mode 100644
index 0000000..98eacb3
--- /dev/null
+++ b/example/build.gradle
@@ -0,0 +1,39 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+
+android {
+ compileSdkVersion 30
+ buildToolsVersion "30.0.2"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: "libs", include: ["*.jar"])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ implementation 'androidx.core:core-ktx:1.3.2'
+ implementation "androidx.cardview:cardview:1.0.0"
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ testImplementation 'junit:junit:4.13.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+
+ implementation project(path: ':bouncy')
+}
\ No newline at end of file
diff --git a/example/consumer-rules.pro b/example/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/example/proguard-rules.pro b/example/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/example/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/example/src/androidTest/java/com/factor/example/ExampleInstrumentedTest.kt b/example/src/androidTest/java/com/factor/example/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000..62ee010
--- /dev/null
+++ b/example/src/androidTest/java/com/factor/example/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.factor.example
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.factor.example.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3c2e881
--- /dev/null
+++ b/example/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/src/main/java/com/factor/example/MainActivity.kt b/example/src/main/java/com/factor/example/MainActivity.kt
new file mode 100644
index 0000000..16c8b6c
--- /dev/null
+++ b/example/src/main/java/com/factor/example/MainActivity.kt
@@ -0,0 +1,13 @@
+package com.factor.example
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+
+class MainActivity : AppCompatActivity()
+{
+ override fun onCreate(savedInstanceState: Bundle?)
+ {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ }
+}
\ No newline at end of file
diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..fa8af20
--- /dev/null
+++ b/example/src/main/res/layout/activity_main.xml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/src/main/res/values/strings.xml b/example/src/main/res/values/strings.xml
new file mode 100644
index 0000000..5984ab1
--- /dev/null
+++ b/example/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Factor Example
+
\ No newline at end of file
diff --git a/example/src/test/java/com/factor/example/ExampleUnitTest.kt b/example/src/test/java/com/factor/example/ExampleUnitTest.kt
new file mode 100644
index 0000000..46a4b56
--- /dev/null
+++ b/example/src/test/java/com/factor/example/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.factor.example
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..c52ac9b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,19 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ef4d539
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Jan 12 12:08:21 PST 2021
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..cccdd3d
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..f955316
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..d697fca
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,4 @@
+include ':example'
+include ':bouncy'
+include ':example'
+rootProject.name = "library"
\ No newline at end of file