|
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|
|
import android.content.Context
|
|
import android.content.Context
|
|
|
import android.text.method.LinkMovementMethod
|
|
import android.text.method.LinkMovementMethod
|
|
|
import android.util.AttributeSet
|
|
import android.util.AttributeSet
|
|
|
|
|
+import android.view.GestureDetector
|
|
|
import android.view.MotionEvent
|
|
import android.view.MotionEvent
|
|
|
import android.view.VelocityTracker
|
|
import android.view.VelocityTracker
|
|
|
import android.view.ViewConfiguration
|
|
import android.view.ViewConfiguration
|
|
@@ -15,21 +16,22 @@ import kotlin.math.abs
|
|
|
import kotlin.math.max
|
|
import kotlin.math.max
|
|
|
import kotlin.math.min
|
|
import kotlin.math.min
|
|
|
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
- * 惯性滚动 TextView
|
|
|
|
|
|
|
+ * 嵌套滚动 TextView
|
|
|
*/
|
|
*/
|
|
|
-@Suppress("unused")
|
|
|
|
|
-open class InertiaScrollTextView @JvmOverloads constructor(
|
|
|
|
|
- context: Context,
|
|
|
|
|
- attrs: AttributeSet? = null
|
|
|
|
|
-) : AppCompatTextView(context, attrs) {
|
|
|
|
|
|
|
+class ScrollTextView(context: Context, attrs: AttributeSet?) :
|
|
|
|
|
+ AppCompatTextView(context, attrs) {
|
|
|
|
|
+
|
|
|
|
|
+ //是否到顶或者到底的标志
|
|
|
|
|
+ private var disallowIntercept = true
|
|
|
|
|
|
|
|
private val scrollStateIdle = 0
|
|
private val scrollStateIdle = 0
|
|
|
private val scrollStateDragging = 1
|
|
private val scrollStateDragging = 1
|
|
|
val scrollStateSettling = 2
|
|
val scrollStateSettling = 2
|
|
|
|
|
|
|
|
private val mViewFling: ViewFling by lazy { ViewFling() }
|
|
private val mViewFling: ViewFling by lazy { ViewFling() }
|
|
|
- private var velocityTracker: VelocityTracker? = null
|
|
|
|
|
|
|
+ private val velocityTracker: VelocityTracker by lazy { VelocityTracker.obtain() }
|
|
|
private var mScrollState = scrollStateIdle
|
|
private var mScrollState = scrollStateIdle
|
|
|
private var mLastTouchY: Int = 0
|
|
private var mLastTouchY: Int = 0
|
|
|
private var mTouchSlop: Int = 0
|
|
private var mTouchSlop: Int = 0
|
|
@@ -46,6 +48,33 @@ open class InertiaScrollTextView @JvmOverloads constructor(
|
|
|
t * t * t * t * t + 1.0f
|
|
t * t * t * t * t + 1.0f
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private val gestureDetector = GestureDetector(context,
|
|
|
|
|
+ object : GestureDetector.SimpleOnGestureListener() {
|
|
|
|
|
+
|
|
|
|
|
+ override fun onDown(e: MotionEvent): Boolean {
|
|
|
|
|
+ disallowIntercept = true
|
|
|
|
|
+ return super.onDown(e)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onScroll(
|
|
|
|
|
+ e1: MotionEvent,
|
|
|
|
|
+ e2: MotionEvent,
|
|
|
|
|
+ distanceX: Float,
|
|
|
|
|
+ distanceY: Float
|
|
|
|
|
+ ): Boolean {
|
|
|
|
|
+ val y = scrollY + distanceY
|
|
|
|
|
+ if (y < 0 || y > mOffsetHeight) {
|
|
|
|
|
+ disallowIntercept = false
|
|
|
|
|
+ //这里触发父布局或祖父布局的滑动事件
|
|
|
|
|
+ parent.requestDisallowInterceptTouchEvent(false)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ disallowIntercept = true
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
init {
|
|
init {
|
|
|
val vc = ViewConfiguration.get(context)
|
|
val vc = ViewConfiguration.get(context)
|
|
|
mTouchSlop = vc.scaledTouchSlop
|
|
mTouchSlop = vc.scaledTouchSlop
|
|
@@ -54,21 +83,13 @@ open class InertiaScrollTextView @JvmOverloads constructor(
|
|
|
movementMethod = LinkMovementMethod.getInstance()
|
|
movementMethod = LinkMovementMethod.getInstance()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- fun atTop(): Boolean {
|
|
|
|
|
- return scrollY <= 0
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- fun atBottom(): Boolean {
|
|
|
|
|
- return scrollY >= mOffsetHeight
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
|
initOffsetHeight()
|
|
initOffsetHeight()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
override fun onTextChanged(
|
|
override fun onTextChanged(
|
|
|
- text: CharSequence?,
|
|
|
|
|
|
|
+ text: CharSequence,
|
|
|
start: Int,
|
|
start: Int,
|
|
|
lengthBefore: Int,
|
|
lengthBefore: Int,
|
|
|
lengthAfter: Int
|
|
lengthAfter: Int
|
|
@@ -77,6 +98,70 @@ open class InertiaScrollTextView @JvmOverloads constructor(
|
|
|
initOffsetHeight()
|
|
initOffsetHeight()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
|
|
|
+ if (lineCount > maxLines) {
|
|
|
|
|
+ gestureDetector.onTouchEvent(event)
|
|
|
|
|
+ }
|
|
|
|
|
+ velocityTracker.addMovement(event)
|
|
|
|
|
+ when (event.action) {
|
|
|
|
|
+ MotionEvent.ACTION_DOWN -> {
|
|
|
|
|
+ setScrollState(scrollStateIdle)
|
|
|
|
|
+ mLastTouchY = (event.y + 0.5f).toInt()
|
|
|
|
|
+ }
|
|
|
|
|
+ MotionEvent.ACTION_MOVE -> {
|
|
|
|
|
+ val y = (event.y + 0.5f).toInt()
|
|
|
|
|
+ var dy = mLastTouchY - y
|
|
|
|
|
+ if (mScrollState != scrollStateDragging) {
|
|
|
|
|
+ var startScroll = false
|
|
|
|
|
+
|
|
|
|
|
+ if (abs(dy) > mTouchSlop) {
|
|
|
|
|
+ if (dy > 0) {
|
|
|
|
|
+ dy -= mTouchSlop
|
|
|
|
|
+ } else {
|
|
|
|
|
+ dy += mTouchSlop
|
|
|
|
|
+ }
|
|
|
|
|
+ startScroll = true
|
|
|
|
|
+ }
|
|
|
|
|
+ if (startScroll) {
|
|
|
|
|
+ setScrollState(scrollStateDragging)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (mScrollState == scrollStateDragging) {
|
|
|
|
|
+ mLastTouchY = y
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ MotionEvent.ACTION_UP -> {
|
|
|
|
|
+ velocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity.toFloat())
|
|
|
|
|
+ val yVelocity = velocityTracker.yVelocity
|
|
|
|
|
+ if (abs(yVelocity) > mMinFlingVelocity) {
|
|
|
|
|
+ mViewFling.fling(-yVelocity.toInt())
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setScrollState(scrollStateIdle)
|
|
|
|
|
+ }
|
|
|
|
|
+ resetTouch()
|
|
|
|
|
+ }
|
|
|
|
|
+ MotionEvent.ACTION_CANCEL -> {
|
|
|
|
|
+ resetTouch()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return super.dispatchTouchEvent(event)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @SuppressLint("ClickableViewAccessibility")
|
|
|
|
|
+ override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
|
|
|
+ val result = super.onTouchEvent(event)
|
|
|
|
|
+ //如果是需要拦截,则再拦截,这个方法会在onScrollChanged方法之后再调用一次
|
|
|
|
|
+ if (disallowIntercept && lineCount > maxLines) {
|
|
|
|
|
+ parent.requestDisallowInterceptTouchEvent(true)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun scrollTo(x: Int, y: Int) {
|
|
|
|
|
+ super.scrollTo(x, min(y, mOffsetHeight))
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private fun initOffsetHeight() {
|
|
private fun initOffsetHeight() {
|
|
|
val mLayoutHeight: Int
|
|
val mLayoutHeight: Int
|
|
|
|
|
|
|
@@ -84,69 +169,23 @@ open class InertiaScrollTextView @JvmOverloads constructor(
|
|
|
val mLayout = layout ?: return
|
|
val mLayout = layout ?: return
|
|
|
//获得内容面板的高度
|
|
//获得内容面板的高度
|
|
|
mLayoutHeight = mLayout.height
|
|
mLayoutHeight = mLayout.height
|
|
|
|
|
+ //获取上内边距
|
|
|
|
|
+ val paddingTop: Int = totalPaddingTop
|
|
|
|
|
+ //获取下内边距
|
|
|
|
|
+ val paddingBottom: Int = totalPaddingBottom
|
|
|
|
|
|
|
|
- //计算滑动距离的边界
|
|
|
|
|
- mOffsetHeight = mLayoutHeight + totalPaddingTop + totalPaddingBottom - measuredHeight
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- override fun scrollTo(x: Int, y: Int) {
|
|
|
|
|
- super.scrollTo(x, min(y, mOffsetHeight))
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ //获得控件的实际高度
|
|
|
|
|
+ val mHeight: Int = measuredHeight
|
|
|
|
|
|
|
|
- @SuppressLint("ClickableViewAccessibility")
|
|
|
|
|
- override fun onTouchEvent(event: MotionEvent?): Boolean {
|
|
|
|
|
- event?.let {
|
|
|
|
|
- if (velocityTracker == null) {
|
|
|
|
|
- velocityTracker = VelocityTracker.obtain()
|
|
|
|
|
- }
|
|
|
|
|
- velocityTracker?.addMovement(it)
|
|
|
|
|
- when (event.action) {
|
|
|
|
|
- MotionEvent.ACTION_DOWN -> {
|
|
|
|
|
- setScrollState(scrollStateIdle)
|
|
|
|
|
- mLastTouchY = (event.y + 0.5f).toInt()
|
|
|
|
|
- }
|
|
|
|
|
- MotionEvent.ACTION_MOVE -> {
|
|
|
|
|
- val y = (event.y + 0.5f).toInt()
|
|
|
|
|
- var dy = mLastTouchY - y
|
|
|
|
|
- if (mScrollState != scrollStateDragging) {
|
|
|
|
|
- var startScroll = false
|
|
|
|
|
-
|
|
|
|
|
- if (abs(dy) > mTouchSlop) {
|
|
|
|
|
- if (dy > 0) {
|
|
|
|
|
- dy -= mTouchSlop
|
|
|
|
|
- } else {
|
|
|
|
|
- dy += mTouchSlop
|
|
|
|
|
- }
|
|
|
|
|
- startScroll = true
|
|
|
|
|
- }
|
|
|
|
|
- if (startScroll) {
|
|
|
|
|
- setScrollState(scrollStateDragging)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (mScrollState == scrollStateDragging) {
|
|
|
|
|
- mLastTouchY = y
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- MotionEvent.ACTION_UP -> {
|
|
|
|
|
- velocityTracker?.computeCurrentVelocity(1000, mMaxFlingVelocity.toFloat())
|
|
|
|
|
- val yVelocity = velocityTracker?.yVelocity ?: 0f
|
|
|
|
|
- if (abs(yVelocity) > mMinFlingVelocity) {
|
|
|
|
|
- mViewFling.fling(-yVelocity.toInt())
|
|
|
|
|
- } else {
|
|
|
|
|
- setScrollState(scrollStateIdle)
|
|
|
|
|
- }
|
|
|
|
|
- resetTouch()
|
|
|
|
|
- }
|
|
|
|
|
- MotionEvent.ACTION_CANCEL -> {
|
|
|
|
|
- resetTouch()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ //计算滑动距离的边界
|
|
|
|
|
+ mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight
|
|
|
|
|
+ if (mOffsetHeight <= 0) {
|
|
|
|
|
+ scrollTo(0, 0)
|
|
|
}
|
|
}
|
|
|
- return super.onTouchEvent(event)
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun resetTouch() {
|
|
private fun resetTouch() {
|
|
|
- velocityTracker?.clear()
|
|
|
|
|
|
|
+ velocityTracker.clear()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private fun setScrollState(state: Int) {
|
|
private fun setScrollState(state: Int) {
|
|
@@ -224,7 +263,7 @@ open class InertiaScrollTextView @JvmOverloads constructor(
|
|
|
mReSchedulePostAnimationCallback = true
|
|
mReSchedulePostAnimationCallback = true
|
|
|
} else {
|
|
} else {
|
|
|
removeCallbacks(this)
|
|
removeCallbacks(this)
|
|
|
- ViewCompat.postOnAnimation(this@InertiaScrollTextView, this)
|
|
|
|
|
|
|
+ ViewCompat.postOnAnimation(this@ScrollTextView, this)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|