diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index 190e31ac9..5e3ab0611 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -1,21 +1,11 @@ package io.legado.app.ui.book.read.page -import android.annotation.SuppressLint import android.content.Context import android.util.AttributeSet -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.AppCompatTextView -import androidx.core.view.ViewCompat -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min +import io.legado.app.ui.widget.text.InertiaScrollTextView -class ContentTextView : AppCompatTextView { +class ContentTextView : InertiaScrollTextView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) @@ -23,43 +13,6 @@ class ContentTextView : AppCompatTextView { constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - private val scrollStateIdle = 0 - private val scrollStateDragging = 1 - val scrollStateSettling = 2 - - private val mViewFling: ViewFling by lazy { ViewFling() } - private var velocityTracker: VelocityTracker? = null - 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 - } - - init { - val vc = ViewConfiguration.get(context) - mTouchSlop = vc.scaledTouchSlop - mMinFlingVelocity = vc.scaledMinimumFlingVelocity - mMaxFlingVelocity = vc.scaledMaximumFlingVelocity - } - - fun atTop(): Boolean { - return scrollY <= 0 - } - - fun atBottom(): Boolean { - return scrollY >= mOffsetHeight - } - /** * 获取当前页总字数 */ @@ -75,171 +28,4 @@ class ContentTextView : AppCompatTextView { return layout?.getLineForVertical(topOfLastLine) ?: 0 } - 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 - - //计算滑动距离的边界 - 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() - } - } - } - return super.onTouchEvent(event) - } - - 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() - } - } - - internal fun postOnAnimation() { - if (mEatRunOnAnimationRequest) { - mReSchedulePostAnimationCallback = true - } else { - removeCallbacks(this) - ViewCompat.postOnAnimation(this@ContentTextView, this) - } - } - } - } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/EventExtensions.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/EventExtensions.kt deleted file mode 100644 index 05d6b28a3..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/EventExtensions.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.legado.app.ui.book.read.page.delegate - -import android.view.MotionEvent - -fun MotionEvent.toAction(action: Int): MotionEvent { - return MotionEvent.obtain( - downTime, - eventTime, - action, - x, - y, - pressure, - size, - metaState, - xPrecision, - yPrecision, - deviceId, - edgeFlags - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt index 9b0ad7c5e..d0ad7bbf8 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt @@ -321,4 +321,21 @@ abstract class PageDelegate(protected val pageView: PageView) { } return hasNext } + + fun MotionEvent.toAction(action: Int): MotionEvent { + return MotionEvent.obtain( + downTime, + eventTime, + action, + x, + y, + pressure, + size, + metaState, + xPrecision, + yPrecision, + deviceId, + edgeFlags + ) + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt new file mode 100644 index 000000000..051d5af4e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt @@ -0,0 +1,231 @@ +package io.legado.app.ui.widget.text + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +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.AppCompatTextView +import androidx.core.view.ViewCompat +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + + +open class InertiaScrollTextView : AppCompatTextView { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) + : super(context, attrs, defStyleAttr) + + + private val scrollStateIdle = 0 + private val scrollStateDragging = 1 + val scrollStateSettling = 2 + + private val mViewFling: ViewFling by lazy { ViewFling() } + private var velocityTracker: VelocityTracker? = null + 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 + } + + init { + val vc = ViewConfiguration.get(context) + mTouchSlop = vc.scaledTouchSlop + mMinFlingVelocity = vc.scaledMinimumFlingVelocity + mMaxFlingVelocity = vc.scaledMaximumFlingVelocity + } + + 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?, + 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 + + //计算滑动距离的边界 + 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() + } + } + } + return super.onTouchEvent(event) + } + + 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() + } + } + + internal fun postOnAnimation() { + if (mEatRunOnAnimationRequest) { + mReSchedulePostAnimationCallback = true + } else { + removeCallbacks(this) + ViewCompat.postOnAnimation(this@InertiaScrollTextView, this) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_text_view.xml b/app/src/main/res/layout/dialog_text_view.xml index 8338a642a..3a2b347c3 100644 --- a/app/src/main/res/layout/dialog_text_view.xml +++ b/app/src/main/res/layout/dialog_text_view.xml @@ -1,5 +1,5 @@ -