This commit is contained in:
kunfei 2022-10-01 12:55:27 +08:00
parent 19722fd6c1
commit 64150d72d0
10 changed files with 393 additions and 268 deletions

View File

@ -13,7 +13,7 @@ import android.text.style.ForegroundColorSpan
import android.text.style.ReplacementSpan
import android.util.AttributeSet
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.regex.Matcher
import java.util.regex.Pattern
@ -21,7 +21,7 @@ import kotlin.math.roundToInt
@Suppress("unused")
class CodeView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
NestScrollMultiAutoCompleteTextView(context, attrs) {
ScrollMultiAutoCompleteTextView(context, attrs) {
private var tabWidth = 0
private var tabWidthInCharacters = 0

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -4,6 +4,7 @@ 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
@ -15,21 +16,22 @@ import kotlin.math.abs
import kotlin.math.max
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 scrollStateDragging = 1
val scrollStateSettling = 2
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 mLastTouchY: Int = 0
private var mTouchSlop: Int = 0
@ -46,6 +48,33 @@ open class InertiaScrollTextView @JvmOverloads constructor(
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
@ -54,21 +83,13 @@ open class InertiaScrollTextView @JvmOverloads constructor(
movementMethod = LinkMovementMethod.getInstance()
}
fun atTop(): Boolean {
return scrollY <= 0
}
fun atBottom(): Boolean {
return scrollY >= mOffsetHeight
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
initOffsetHeight()
}
override fun onTextChanged(
text: CharSequence?,
text: CharSequence,
start: Int,
lengthBefore: Int,
lengthAfter: Int
@ -77,6 +98,70 @@ open class InertiaScrollTextView @JvmOverloads constructor(
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
@ -84,69 +169,23 @@ open class InertiaScrollTextView @JvmOverloads constructor(
val mLayout = layout ?: return
//获得内容面板的高度
mLayoutHeight = mLayout.height
//获取上内边距
val paddingTop: Int = totalPaddingTop
//获取下内边距
val paddingBottom: Int = totalPaddingBottom
//获得控件的实际高度
val mHeight: Int = measuredHeight
//计算滑动距离的边界
mOffsetHeight = mLayoutHeight + totalPaddingTop + totalPaddingBottom - measuredHeight
}
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()
}
}
mOffsetHeight = mLayoutHeight + paddingTop + paddingBottom - mHeight
if (mOffsetHeight <= 0) {
scrollTo(0, 0)
}
return super.onTouchEvent(event)
}
private fun resetTouch() {
velocityTracker?.clear()
velocityTracker.clear()
}
private fun setScrollState(state: Int) {
@ -224,7 +263,7 @@ open class InertiaScrollTextView @JvmOverloads constructor(
mReSchedulePostAnimationCallback = true
} else {
removeCallbacks(this)
ViewCompat.postOnAnimation(this@InertiaScrollTextView, this)
ViewCompat.postOnAnimation(this@ScrollTextView, this)
}
}
}

View File

@ -355,7 +355,7 @@
</LinearLayout>
<io.legado.app.ui.widget.text.NestScrollTextView
<io.legado.app.ui.widget.text.ScrollTextView
android:id="@+id/tv_intro"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -331,7 +331,7 @@
</LinearLayout>
<io.legado.app.ui.widget.text.NestScrollTextView
<io.legado.app.ui.widget.text.ScrollTextView
android:id="@+id/tv_intro"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -5,7 +5,7 @@
android:layout_height="match_parent"
android:background="@color/background">
<io.legado.app.ui.widget.text.InertiaScrollTextView
<io.legado.app.ui.widget.text.ScrollTextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -13,7 +13,7 @@
app:popupTheme="@style/AppTheme.PopupOverlay"
app:titleTextAppearance="@style/ToolbarTitle" />
<io.legado.app.ui.widget.text.InertiaScrollTextView
<io.legado.app.ui.widget.text.ScrollTextView
android:id="@+id/text_view"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -9,6 +9,7 @@
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine" />
android:inputType="textMultiLine"
android:maxLines="12" />
</io.legado.app.ui.widget.text.TextInputLayout>