pull/2361/head
parent
19722fd6c1
commit
64150d72d0
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
Loading…
Reference in new issue