mirror of
https://github.com/gedoor/legado.git
synced 2024-07-17 00:58:29 +08:00
优化
This commit is contained in:
parent
19722fd6c1
commit
64150d72d0
@ -13,7 +13,7 @@ import android.text.style.ForegroundColorSpan
|
|||||||
import android.text.style.ReplacementSpan
|
import android.text.style.ReplacementSpan
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import io.legado.app.ui.widget.text.NestScrollMultiAutoCompleteTextView
|
import io.legado.app.ui.widget.text.ScrollMultiAutoCompleteTextView
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.regex.Matcher
|
import java.util.regex.Matcher
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
@ -21,7 +21,7 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class CodeView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
class CodeView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
|
||||||
NestScrollMultiAutoCompleteTextView(context, attrs) {
|
ScrollMultiAutoCompleteTextView(context, attrs) {
|
||||||
|
|
||||||
private var tabWidth = 0
|
private var tabWidth = 0
|
||||||
private var tabWidthInCharacters = 0
|
private var tabWidthInCharacters = 0
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
package io.legado.app.ui.widget.text
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.GestureDetector
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 嵌套滚动 MultiAutoCompleteTextView
|
|
||||||
*/
|
|
||||||
open class NestScrollMultiAutoCompleteTextView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null
|
|
||||||
) : AppCompatMultiAutoCompleteTextView(context, attrs) {
|
|
||||||
|
|
||||||
//是否到顶或者到底的标志
|
|
||||||
private var disallowIntercept = true
|
|
||||||
|
|
||||||
//滑动距离的最大边界
|
|
||||||
private var mOffsetHeight = 0
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
||||||
initOffsetHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(
|
|
||||||
text: CharSequence,
|
|
||||||
start: Int,
|
|
||||||
lengthBefore: Int,
|
|
||||||
lengthAfter: Int
|
|
||||||
) {
|
|
||||||
super.onTextChanged(text, start, lengthBefore, lengthAfter)
|
|
||||||
initOffsetHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
if (lineCount > maxLines) {
|
|
||||||
gestureDetector.onTouchEvent(event)
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initOffsetHeight() {
|
|
||||||
val mLayoutHeight: Int
|
|
||||||
|
|
||||||
//获得内容面板
|
|
||||||
val mLayout = layout ?: return
|
|
||||||
//获得内容面板的高度
|
|
||||||
mLayoutHeight = mLayout.height
|
|
||||||
//获取上内边距
|
|
||||||
val paddingTop: Int = totalPaddingTop
|
|
||||||
//获取下内边距
|
|
||||||
val paddingBottom: Int = totalPaddingBottom
|
|
||||||
|
|
||||||
//获得控件的实际高度
|
|
||||||
val mHeight: Int = measuredHeight
|
|
||||||
|
|
||||||
//计算滑动距离的边界
|
|
||||||
mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight
|
|
||||||
if (mOffsetHeight <= 0) {
|
|
||||||
scrollTo(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
package io.legado.app.ui.widget.text
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 嵌套滚动 TextView
|
|
||||||
*/
|
|
||||||
class NestScrollTextView(context: Context, attrs: AttributeSet?) :
|
|
||||||
AppCompatTextView(context, attrs) {
|
|
||||||
//滑动距离的最大边界
|
|
||||||
private var mOffsetHeight = 0
|
|
||||||
|
|
||||||
//是否到顶或者到底的标志
|
|
||||||
private var mBottomFlag = false
|
|
||||||
|
|
||||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
||||||
initOffsetHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(
|
|
||||||
text: CharSequence,
|
|
||||||
start: Int,
|
|
||||||
lengthBefore: Int,
|
|
||||||
lengthAfter: Int
|
|
||||||
) {
|
|
||||||
super.onTextChanged(text, start, lengthBefore, lengthAfter)
|
|
||||||
initOffsetHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initOffsetHeight() {
|
|
||||||
val mLayoutHeight: Int
|
|
||||||
|
|
||||||
//获得内容面板
|
|
||||||
val mLayout = layout ?: return
|
|
||||||
//获得内容面板的高度
|
|
||||||
mLayoutHeight = mLayout.height
|
|
||||||
//获取上内边距
|
|
||||||
val paddingTop: Int = totalPaddingTop
|
|
||||||
//获取下内边距
|
|
||||||
val paddingBottom: Int = totalPaddingBottom
|
|
||||||
|
|
||||||
//获得控件的实际高度
|
|
||||||
val mHeight: Int = measuredHeight
|
|
||||||
|
|
||||||
//计算滑动距离的边界
|
|
||||||
mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight
|
|
||||||
if (mOffsetHeight <= 0) {
|
|
||||||
scrollTo(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
|
||||||
//如果是新的按下事件,则对mBottomFlag重新初始化
|
|
||||||
mBottomFlag = mOffsetHeight <= 0
|
|
||||||
}
|
|
||||||
return super.dispatchTouchEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
||||||
val result = super.onTouchEvent(event)
|
|
||||||
//如果是需要拦截,则再拦截,这个方法会在onScrollChanged方法之后再调用一次
|
|
||||||
if (!mBottomFlag) parent.requestDisallowInterceptTouchEvent(true)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScrollChanged(horiz: Int, vert: Int, oldHoriz: Int, oldVert: Int) {
|
|
||||||
super.onScrollChanged(horiz, vert, oldHoriz, oldVert)
|
|
||||||
if (vert == mOffsetHeight || vert == 0) {
|
|
||||||
//这里触发父布局或祖父布局的滑动事件
|
|
||||||
parent.requestDisallowInterceptTouchEvent(false)
|
|
||||||
mBottomFlag = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,272 @@
|
|||||||
|
package io.legado.app.ui.widget.text
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.VelocityTracker
|
||||||
|
import android.view.ViewConfiguration
|
||||||
|
import android.view.animation.Interpolator
|
||||||
|
import android.widget.OverScroller
|
||||||
|
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 嵌套滚动 MultiAutoCompleteTextView
|
||||||
|
*/
|
||||||
|
open class ScrollMultiAutoCompleteTextView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null
|
||||||
|
) : AppCompatMultiAutoCompleteTextView(context, attrs) {
|
||||||
|
|
||||||
|
//是否到顶或者到底的标志
|
||||||
|
private var disallowIntercept = true
|
||||||
|
|
||||||
|
private val scrollStateIdle = 0
|
||||||
|
private val scrollStateDragging = 1
|
||||||
|
val scrollStateSettling = 2
|
||||||
|
|
||||||
|
private val mViewFling: ViewFling by lazy { ViewFling() }
|
||||||
|
private val velocityTracker: VelocityTracker by lazy { VelocityTracker.obtain() }
|
||||||
|
private var mScrollState = scrollStateIdle
|
||||||
|
private var mLastTouchY: Int = 0
|
||||||
|
private var mTouchSlop: Int = 0
|
||||||
|
private var mMinFlingVelocity: Int = 0
|
||||||
|
private var mMaxFlingVelocity: Int = 0
|
||||||
|
|
||||||
|
//滑动距离的最大边界
|
||||||
|
private var mOffsetHeight: Int = 0
|
||||||
|
|
||||||
|
//f(x) = (x-1)^5 + 1
|
||||||
|
private val sQuinticInterpolator = Interpolator {
|
||||||
|
var t = it
|
||||||
|
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 {
|
||||||
|
val vc = ViewConfiguration.get(context)
|
||||||
|
mTouchSlop = vc.scaledTouchSlop
|
||||||
|
mMinFlingVelocity = vc.scaledMinimumFlingVelocity
|
||||||
|
mMaxFlingVelocity = vc.scaledMaximumFlingVelocity
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||||
|
initOffsetHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTextChanged(
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
lengthBefore: Int,
|
||||||
|
lengthAfter: Int
|
||||||
|
) {
|
||||||
|
super.onTextChanged(text, start, lengthBefore, lengthAfter)
|
||||||
|
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() {
|
||||||
|
val mLayoutHeight: Int
|
||||||
|
|
||||||
|
//获得内容面板
|
||||||
|
val mLayout = layout ?: return
|
||||||
|
//获得内容面板的高度
|
||||||
|
mLayoutHeight = mLayout.height
|
||||||
|
//获取上内边距
|
||||||
|
val paddingTop: Int = totalPaddingTop
|
||||||
|
//获取下内边距
|
||||||
|
val paddingBottom: Int = totalPaddingBottom
|
||||||
|
|
||||||
|
//获得控件的实际高度
|
||||||
|
val mHeight: Int = measuredHeight
|
||||||
|
|
||||||
|
//计算滑动距离的边界
|
||||||
|
mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight
|
||||||
|
if (mOffsetHeight <= 0) {
|
||||||
|
scrollTo(0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetTouch() {
|
||||||
|
velocityTracker.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setScrollState(state: Int) {
|
||||||
|
if (state == mScrollState) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mScrollState = state
|
||||||
|
if (state != scrollStateSettling) {
|
||||||
|
mViewFling.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 惯性滚动
|
||||||
|
*/
|
||||||
|
private inner class ViewFling : Runnable {
|
||||||
|
|
||||||
|
private var mLastFlingY = 0
|
||||||
|
private val mScroller: OverScroller = OverScroller(context, sQuinticInterpolator)
|
||||||
|
private var mEatRunOnAnimationRequest = false
|
||||||
|
private var mReSchedulePostAnimationCallback = false
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
disableRunOnAnimationRequests()
|
||||||
|
val scroller = mScroller
|
||||||
|
if (scroller.computeScrollOffset()) {
|
||||||
|
val y = scroller.currY
|
||||||
|
val dy = y - mLastFlingY
|
||||||
|
mLastFlingY = y
|
||||||
|
if (dy < 0 && scrollY > 0) {
|
||||||
|
scrollBy(0, max(dy, -scrollY))
|
||||||
|
} else if (dy > 0 && scrollY < mOffsetHeight) {
|
||||||
|
scrollBy(0, min(dy, mOffsetHeight - scrollY))
|
||||||
|
}
|
||||||
|
postOnAnimation()
|
||||||
|
}
|
||||||
|
enableRunOnAnimationRequests()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fling(velocityY: Int) {
|
||||||
|
mLastFlingY = 0
|
||||||
|
setScrollState(scrollStateSettling)
|
||||||
|
mScroller.fling(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
velocityY,
|
||||||
|
Integer.MIN_VALUE,
|
||||||
|
Integer.MAX_VALUE,
|
||||||
|
Integer.MIN_VALUE,
|
||||||
|
Integer.MAX_VALUE
|
||||||
|
)
|
||||||
|
postOnAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
removeCallbacks(this)
|
||||||
|
mScroller.abortAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disableRunOnAnimationRequests() {
|
||||||
|
mReSchedulePostAnimationCallback = false
|
||||||
|
mEatRunOnAnimationRequest = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enableRunOnAnimationRequests() {
|
||||||
|
mEatRunOnAnimationRequest = false
|
||||||
|
if (mReSchedulePostAnimationCallback) {
|
||||||
|
postOnAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun postOnAnimation() {
|
||||||
|
if (mEatRunOnAnimationRequest) {
|
||||||
|
mReSchedulePostAnimationCallback = true
|
||||||
|
} else {
|
||||||
|
removeCallbacks(this)
|
||||||
|
ViewCompat.postOnAnimation(this@ScrollMultiAutoCompleteTextView, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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")
|
class ScrollTextView(context: Context, attrs: AttributeSet?) :
|
||||||
open class InertiaScrollTextView @JvmOverloads constructor(
|
AppCompatTextView(context, attrs) {
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null
|
//是否到顶或者到底的标志
|
||||||
) : 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
|
||||||
|
|
||||||
|
//获得控件的实际高度
|
||||||
|
val mHeight: Int = measuredHeight
|
||||||
|
|
||||||
//计算滑动距离的边界
|
//计算滑动距离的边界
|
||||||
mOffsetHeight = mLayoutHeight + totalPaddingTop + totalPaddingBottom - measuredHeight
|
mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight
|
||||||
}
|
if (mOffsetHeight <= 0) {
|
||||||
|
scrollTo(0, 0)
|
||||||
override fun scrollTo(x: Int, y: Int) {
|
|
||||||
super.scrollTo(x, min(y, mOffsetHeight))
|
|
||||||
}
|
|
||||||
|
|
||||||
@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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -355,7 +355,7 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<io.legado.app.ui.widget.text.NestScrollTextView
|
<io.legado.app.ui.widget.text.ScrollTextView
|
||||||
android:id="@+id/tv_intro"
|
android:id="@+id/tv_intro"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -331,7 +331,7 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<io.legado.app.ui.widget.text.NestScrollTextView
|
<io.legado.app.ui.widget.text.ScrollTextView
|
||||||
android:id="@+id/tv_intro"
|
android:id="@+id/tv_intro"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/background">
|
android:background="@color/background">
|
||||||
|
|
||||||
<io.legado.app.ui.widget.text.InertiaScrollTextView
|
<io.legado.app.ui.widget.text.ScrollTextView
|
||||||
android:id="@+id/text_view"
|
android:id="@+id/text_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
app:popupTheme="@style/AppTheme.PopupOverlay"
|
app:popupTheme="@style/AppTheme.PopupOverlay"
|
||||||
app:titleTextAppearance="@style/ToolbarTitle" />
|
app:titleTextAppearance="@style/ToolbarTitle" />
|
||||||
|
|
||||||
<io.legado.app.ui.widget.text.InertiaScrollTextView
|
<io.legado.app.ui.widget.text.ScrollTextView
|
||||||
android:id="@+id/text_view"
|
android:id="@+id/text_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
android:id="@+id/editText"
|
android:id="@+id/editText"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:inputType="textMultiLine" />
|
android:inputType="textMultiLine"
|
||||||
|
android:maxLines="12" />
|
||||||
|
|
||||||
</io.legado.app.ui.widget.text.TextInputLayout>
|
</io.legado.app.ui.widget.text.TextInputLayout>
|
Loading…
Reference in New Issue
Block a user