Android custom View inertial scrolling effect (without using Scroller)

Foreword:

  • I have seen many inertial scrolling solutions on the Internet, all of which are implemented through Scroller and computeScroll. However, in actual development, there may be some scenarios that are inappropriate, such as coordinated layout, and internal sub-Views have particularly complex linkage effects, which need to be coordinated through offsets.
  • The implementation principle is very simple: use VelocityTracker (velocity tracker) to get the process of the initial speedgradually decreasing until it stops. I wrote two versions: Rough version and RecyclerView version. The difference is that the former is calculated by me myself, and the latter is related to the inertial motion in RecyclerView. The code is extracted and encapsulated. It is recommended to use the latter.

1. Rough version

  • FlingTask (core class)
/**
 * Inertial tasks
 */
public class FlingTask implements Runnable {

    private Handler mHandler;
    private int velocityY = 0;
    private int originalVelocityY = 0;
    private FlingTaskCallback flingTaskCallback;

    public FlingTask(int velocityY, Handler handler, FlingTaskCallback callback) {
        this.velocityY = velocityY;
        this.mHandler = handler;
        this.originalVelocityY = velocityY;
        this.flingTaskCallback = callback;
    }

    boolean initSlide = false; // Initialize sliding
    int average = 0; // average speed
    int tempAverage = 1;
    boolean startSmooth = false; // Start decreasing speed smoothing
    int sameCount = 0; // Number of times the values are the same

    //Here controls the average sliding speed of each segment
    private int getAverageDistance(int velocityY) {
        int t = velocityY;
        if (t < 470) {
            t /= 21;
        }
        //divide by zero
        if (t == 0) return 0;
        int v = Math.abs(velocityY / t);
        if (v < 21) {
            t /= 21;
            if (t > 20) {
                t /= 5;
            }
        }
        return t;
    }

    @Override
    public void run() {
        // The task ends only when the speed is completely consumed, and does not conflict with the end of view scrolling.
        // This judgment is for expansion, transferring the unused speed to the specified scroll view
        // if (velocityY > 0) {

        //As soon as the view scroll ends, end the task immediately
        if (tempAverage > 0 & amp; & amp; velocityY > 0) {

            if (!initSlide) {
                average = getAverageDistance(velocityY);
                initSlide = true;
            }

            float progress = (float) velocityY / originalVelocityY;
            float newProgress = 0f;
            if (average > 300) {
                newProgress = getInterpolation(progress);
            } else {
                newProgress = getInterpolation02(progress);
            }

            int prTemp = tempAverage;
            if (!startSmooth) tempAverage = (int) (average * newProgress);

            // Decreasing speed smoothing
            if (prTemp == tempAverage) {
                sameCount + + ;
                if (sameCount > 1 & amp; & amp; tempAverage > 0) { // The larger this value is, the stiffer the final attenuation will be when it stops, 0 - 30
                    tempAverage--;
                    sameCount = 0;
                    startSmooth = true;
                }
            }

            flingTaskCallback.executeTask(tempAverage);

            velocityY -= tempAverage;

            // This is written here for expansion, and the unused speed is transferred to other scrolling lists.
            // The judgment statement needs to be changed to if (velocityY > 0)
            if (tempAverage == 0) { // When view scrolling stops
                // If the speed is not exhausted, continue to consume it
                velocityY -= average;
            }
            // Log.d("TAG", "tempAverage: " + tempAverage + " --- velocityY: " + velocityY + " --- originalVelocityY: " + originalVelocityY);

            mHandler.post(this);
        } else {
            flingTaskCallback.stopTask();
            stopTask();
        }
    }

    public void stopTask() {
        mHandler.removeCallbacks(this);
        initSlide = false;
        startSmooth = false;
    }


    // From acceleration to gradual attenuation (AccelerateDecelerateInterpolator interpolator core source code)
    // rate of change starts and ends slowly but accelerates in the middle
    public float getInterpolation(float input) {
        return (float) (Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }

    //The speed gradually decays (DecelerateInterpolator interpolator core source code)
    public float getInterpolation02(float input) {
        return (float) (1.0f - (1.0f - input) * (1.0f - input));
    }

    public interface FlingTaskCallback {
        void executeTask(int dy);

        void stopTask();
    }
}

2. RecyclerView version

  • ComputeFling (core class)
/**
 * Calculate the current inertia, the total distance generated and the duration
 * Use it with ValueAnimator to achieve inertia effect
 */
public class ComputeFling {

    private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
    private static final int NB_SAMPLES = 100;

    // The amount of friction exerted by inertia on rolling and whipping.
    // Return value: A scalar dimensionless value representing the friction coefficient.
    // Fling friction
    private float mFlingFriction = ViewConfiguration.getScrollFriction();

    // A context-specific coefficient adjusted to physical values.
    private float mPhysicalCoeff;

    // deceleration rate
    private float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));

    // Distance to travel along spline animation
    private int mSplineDistance; // Total distance generated by inertia

    // Duration to complete spline component of animation
    private int mSplineDuration; // Inertia duration

    // Initial velocity
    private int mVelocity; // speed

    // Current velocity
    private float mCurrVelocity;

    // Animation starting time, in system milliseconds
    private long mStartTime;

    // Animation duration, in milliseconds
    private int mDuration;

    public ComputeFling(Context context) {
        // pixel density
        final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
        mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2) Gravity acceleration unit: m/s2; gravitational acceleration is the acceleration of an object under the action of gravity, also called free fall acceleration, represented by g.
                * 39.37f // inch/meter
                *ppi
                * 0.84f; // look and feel tuning
    }

    public FlingTaskInfo compute(int start, int velocity, int min, int max) {
        mCurrVelocity = mVelocity = velocity;
        mDuration = mSplineDuration = 0;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();

        double totalDistance = 0.0;

        if (velocity != 0) {
            mDuration = mSplineDuration = getSplineFlingDuration(velocity);
            totalDistance = getSplineFlingDistance(velocity);
            return new FlingTaskInfo(mDuration, totalDistance);
        }
        return new FlingTaskInfo(0, 0);
    }

    private double getSplineDeceleration(int velocity) {
        // Math.abs: Get the absolute value
        // Math.log: Calculate logarithm
        return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
    }

    /**
     * @param velocity speed
     * @return current inertia duration
     */
    private int getSplineFlingDuration(int velocity) {
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        // Calculate exponential function
        return (int) (1000.0 * Math.exp(l / decelMinusOne));
    }

    /**
     * @param velocity speed
     * @return the total distance generated by the current inertia
     */
    private double getSplineFlingDistance(int velocity) {
        final double l = getSplineDeceleration(velocity);
        final double decelMinusOne = DECELERATION_RATE - 1.0;
        return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
    }

    /**
     * Inertia parameter entity
     */
    public class FlingTaskInfo {
        private int mDuration; // The duration of this inertial scrolling
        private double totalDistance; // This inertial scrolling, the total scrolling distance

        public FlingTaskInfo(int mDuration, double totalDistance) {
            this.mDuration = mDuration;
            this.totalDistance = totalDistance;
        }

        public int getmDuration() {
            return mDuration;
        }

        public double getTotalDistance() {
            return totalDistance;
        }

        @Override
        public String toString() {
            return "FlingTaskInfo{" +
                    "mDuration=" + mDuration +
                    ", totalDistance=" + totalDistance +
                    '}';
        }
    }

    /**
     * Decreasing interpolator
     */
    public static final Interpolator sQuinticInterpolator = new Interpolator() {
        @Override
        public float getInterpolation(float t) {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        }
    };

}

3. Source code address

https://github.com/LanSeLianMa/FlingScrollView/tree/master