termux / termux-app

Termux - a terminal emulator application for Android OS extendible by variety of packages.
https://f-droid.org/en/packages/com.termux
Other
36.13k stars 3.79k forks source link

[Feature]: over-scroll and nested-scrolling animation in termux terminal-emulator #3826

Open RohitVerma882 opened 9 months ago

RohitVerma882 commented 9 months ago

Feature description

Add over scroll animation feature in termux terminal for Android 12+ devices

Additional information

In mt manager have this feature

tusharhero commented 8 months ago

What exactly do you mean by "over scroll animation"?

tusharhero commented 8 months ago

https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior Are you talking about this?

RohitVerma882 commented 8 months ago

https://github.com/termux/termux-app/assets/56387351/0b42dd3c-6cc8-4dc8-aa7e-e9591bae6498

tusharhero commented 8 months ago

I am not able to play this video.

sylirre commented 8 months ago

Plays well on Google Chrome and Firefox Focus.

lzhiyong commented 5 months ago

How to implements overscroll stretch animation for custom view on android 12 or later, https://developer.android.com/develop/ui/views/touch-and-input/gestures/scroll#kotlin

InteractiveChart sample https://android.googlesource.com/platform/development/+/master/samples/training/InteractiveChart/src/com/example/android/interactivechart/InteractiveLineGraphView.java

RohitVerma882 commented 5 months ago

How to implements overscroll stretch animation for custom view on android 12 or later, https://developer.android.com/develop/ui/views/touch-and-input/gestures/scroll#kotlin

I don't know how to implement this on terminal-emulator

lzhiyong commented 5 months ago

I don't know how to implement this on terminal-emulator

@RohitVerma882 please refer to the InteractiveChart sample

lzhiyong commented 5 months ago

@RohitVerma882 My App implments example code snippet


// ... imports

open class OverScrollView @JvmOverloads constructor(
    context: Context, 
    attrs: AttributeSet? = null, 
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), 
    GestureDetector.OnGestureListener  {

    private val scroller: OverScroller
    private val gestureDetector: GestureDetector

    // Edge effect / overscroll tracking objects.
    private val edgeEffectTop: EdgeEffect
    private val edgeEffectBottom: EdgeEffect
    private val edgeEffectLeft: EdgeEffect
    private val edgeEffectRight: EdgeEffect

    private var edgeEffectTopActive = false
    private var edgeEffectBottomActive = false
    private var edgeEffectLeftActive = false
    private var edgeEffectRightActive = false

    // for example the max scrollX and scrollY
    // screen width / 2
    private val maxScrollX: Int
        get() = context.resources.displayMetrics.widthPixels / 2
    // screen height / 2
    private val maxScrollY: Int
        get() = context.resources.displayMetrics.heightPixels / 2

    init {
        scroller = OverScroller(context)
        gestureDetector = GestureDetector(context, this)

        // Sets up edge effects
        edgeEffectTop = EdgeEffect(context)
        edgeEffectBottom = EdgeEffect(context)
        edgeEffectLeft = EdgeEffect(context)
        edgeEffectRight = EdgeEffect(context)
    }

    override fun computeScroll() {
        if (scroller.computeScrollOffset()) {

            // horizontal absorb           
            if (
                scroller.currX < 0 &&
                edgeEffectLeft.isFinished() &&
                !edgeEffectLeftActive
            ) {                
                edgeEffectLeft.onAbsorb(scroller.currVelocity.toInt())
                edgeEffectLeftActive = true
            } else if (
                scroller.currX > maxScrollX &&
                edgeEffectRight.isFinished() &&
                !edgeEffectRightActive
            ) {
                edgeEffectRight.onAbsorb(scroller.currVelocity.toInt())
                edgeEffectRightActive = true               
            }           

            // vertical absorb
            if (
                scroller.currY < 0 &&
                edgeEffectTop.isFinished() &&
                !edgeEffectTopActive
            ) {
                edgeEffectTop.onAbsorb(scroller.currVelocity.toInt())
                edgeEffectTopActive = true                
            } else if (
                scroller.currY > maxScrollY &&
                edgeEffectBottom.isFinished() &&
                !edgeEffectBottomActive
            ) {                
                edgeEffectBottom.onAbsorb(scroller.currVelocity.toInt())
                edgeEffectBottomActive = true
            }

            scrollTo(
               Math.min(Math.max(scroller.currX, 0), maxScrollX), 
               Math.min(Math.max(scroller.currY, 0), maxScrollY)
            )           

            postInvalidateOnAnimation()
        }
    }

    /**
     * Draws the overscroll "glow" at the four edges of the chart region, if necessary. The edges
     * of the chart region are stored in {@link #mContentRect}.
     *
     * @see EdgeEffect
     */
    private fun drawEdgeEffect(canvas: Canvas) {
        // The methods below rotate and translate the canvas as needed before drawing the glow,
        // since the EdgeEffect always draws a top-glow at 0,0.
        var needsInvalidate = false

        if (!edgeEffectTop.isFinished()) {
            val restoreCount = canvas.save()
            canvas.translate(0f, 0f)
            edgeEffectTop.setSize(getWidth(), getHeight())
            if (edgeEffectTop.draw(canvas)) {
                needsInvalidate = true
            }
            canvas.restoreToCount(restoreCount)
        }

        if (!edgeEffectBottom.isFinished()) {
            val restoreCount = canvas.save()
            canvas.translate(-getWidth().toFloat(), getHeight().toFloat())
            edgeEffectBottom.setSize(getWidth(), getHeight())
            canvas.rotate(180f, getWidth().toFloat(), 0f)

            if (edgeEffectBottom.draw(canvas)) {
                needsInvalidate = true
            }
            canvas.restoreToCount(restoreCount)
        }

        if (!edgeEffectLeft.isFinished()) {
            val restoreCount = canvas.save()
            canvas.translate(0f, getHeight().toFloat())
            canvas.rotate(-90f, 0f, 0f);
            edgeEffectLeft.setSize(getHeight(), getWidth())
            if (edgeEffectLeft.draw(canvas)) {
                needsInvalidate = true
            }
            canvas.restoreToCount(restoreCount)
        }

        if (!edgeEffectRight.isFinished()) {
            val restoreCount = canvas.save()
            canvas.translate(getWidth().toFloat(), 0f)
            canvas.rotate(90f, 0f, 0f)
            edgeEffectRight.setSize(getHeight(), getWidth())
            if (edgeEffectRight.draw(canvas)) {
                needsInvalidate = true
            }
            canvas.restoreToCount(restoreCount)
        }

        if (needsInvalidate) {
            postInvalidateOnAnimation()
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // draw the edge effect
        drawEdgeEffect(canvas)
    }

    private fun releaseEdgeEffects() {
        edgeEffectTop.onRelease()
        edgeEffectBottom.onRelease()
        edgeEffectLeft.onRelease()
        edgeEffectRight.onRelease()
    }

    private fun passtiveEdgeEffects() {
        edgeEffectTopActive = false
        edgeEffectBottomActive = false
        edgeEffectLeftActive = false
        edgeEffectRightActive = false
    }

    override fun onTouchEvent(e: MotionEvent): Boolean {
        when (e.action) {
            MotionEvent.ACTION_DOWN -> { 
                scroller.abortAnimation() 
            }
            MotionEvent.ACTION_MOVE -> {
                // do something
            }
            MotionEvent.ACTION_UP -> {
                // callback the onUp
                onUp(e) 
            }
        }

        gestureDetector.onTouchEvent(e)
        return true
    }

    override fun onDown(e: MotionEvent): Boolean {
        // when the user touches the screen
        // change the edge effect states
        passtiveEdgeEffects()
        return true
    }

    override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
        scroller.startScroll(
            scroller.currX, 
            scroller.currY,
            if(Math.abs(distanceX) > Math.abs(distanceY)) distanceX.toInt() else 0,
            if(Math.abs(distanceY) > Math.abs(distanceX)) distanceY.toInt() else 0,
            0
        )

        // vertical stretch overscroll effect 
        if (scroller.currY + distanceY < 0f) {                                          
            edgeEffectTop.onPullDistance(-distanceY / getHeight(), 1 - e2.getX() / getWidth())
            edgeEffectTopActive = true
        } else if (scroller.currY + distanceY > maxScrollY.toFloat()) {                                
            edgeEffectBottom.onPullDistance(distanceY / getHeight(), e2.getX() / getWidth())
            edgeEffectBottomActive = true
        }

        // horizontal stretch overscroll effect
        if (scroller.currX + distanceX < 0f) {                       
            edgeEffectLeft.onPullDistance(-distanceX / getWidth(), e2.getY() / getHeight())
            edgeEffectLeftActive = true
        } else if (scroller.currX + distanceX > maxScrollX.toFloat()) {                
            edgeEffectRight.onPullDistance(distanceX / getWidth(), e2.getY() / getHeight())
            edgeEffectRightActive = true
        }
        return true
    }

    override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
        // Before flinging, stops the current animation.
        scroller.forceFinished(true)

        scroller.fling(
            // Current scroll position
            scrollX, 
            scrollY, 
            if(Math.abs(velocityX) > Math.abs(velocityY)) -velocityX.toInt() else 0, 
            if(Math.abs(velocityY) > Math.abs(velocityX)) -velocityY.toInt() else 0,
            /*
             * Minimum and maximum scroll positions. The minimum scroll
             * position is generally 0 and the maximum scroll position
             * is generally the content size less the screen size. So if the
             * content width is 2000 pixels and the screen width is 1200
             * pixels, the maximum scroll offset is 800 pixels.
             */
            0, maxScrollX, 
            0, maxScrollY,
            // The edges of the content. This comes into play when using
            // the EdgeEffect class to draw "glow" overlays.
            getWidth() / 10, 
            getHeight() / 10
        )
        postInvalidateOnAnimation()
        return true
    }

    private fun onUp(e: MotionEvent) {
        // when the user lifts their finger off the screen
        // release the edge effects
        releaseEdgeEffects()
    }
}
lzhiyong commented 5 months ago

https://github.com/termux/termux-app/assets/36750502/09f77fea-6cf5-4fd4-9b35-507b9184ef1f

paiwand61 commented 5 months ago

hi