Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
RohitVerma882 opened this issue Feb 7, 2024 · 11 comments

Comments

@RohitVerma882
Copy link

Feature description

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

Additional information

In mt manager have this feature

@tusharhero
Copy link

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

@tusharhero
Copy link

@RohitVerma882
Copy link
Author

VID_20240213114751.mp4

@tusharhero
Copy link

I am not able to play this video.

@sylirre
Copy link
Member

sylirre commented Feb 13, 2024

Plays well on Google Chrome and Firefox Focus.

@lzhiyong
Copy link

lzhiyong commented May 9, 2024

@RohitVerma882
Copy link
Author

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

@RohitVerma882 RohitVerma882 changed the title [Feature]: over scroll in termux terminal [Feature]: over-scroll and nested-scrolling animation in termux terminal-emulator May 9, 2024
@lzhiyong
Copy link

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

@RohitVerma882 please refer to the InteractiveChart sample

@lzhiyong
Copy link

lzhiyong commented May 16, 2024

@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
Copy link

Record_2024-05-16-14-01-36_01748b7c4d72684b5ef41a131992be3c.mp4

@paiwand61
Copy link

hi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants