Question

I've created an application for Android, in which there is a NumberPicker. And I need to change the value of this NumberPicker but with a smooth animation like when you touch it and change its value.

For example, assume the current value is 1 and it's going to be 5, I want the NumberPicker to spin from 1 to 2 then to 3 and so on. But I want it to spin not just instantly change the values!

If I use the following code:

numberPicker.setValue(5);

its value will instantly change to 5, but I want it to roll up from 1 to 5 like when you manually touch it and make it spin.

Was it helpful?

Solution

I solved it via refelction

        /**
         * using reflection to change the value because
         * changeValueByOne is a private function and setValue
         * doesn't call the onValueChange listener.
         * 
         * @param higherPicker
         *            the higher picker
         * @param increment
         *            the increment
         */
        private void changeValueByOne(final NumberPicker higherPicker, final boolean increment) {

            Method method;
            try {
                // refelction call for
                // higherPicker.changeValueByOne(true);
                method = higherPicker.getClass().getDeclaredMethod("changeValueByOne", boolean.class);
                method.setAccessible(true);
                method.invoke(higherPicker, increment);

            } catch (final NoSuchMethodException e) {
                e.printStackTrace();
            } catch (final IllegalArgumentException e) {
                e.printStackTrace();
            } catch (final IllegalAccessException e) {
                e.printStackTrace();
            } catch (final InvocationTargetException e) {
                e.printStackTrace();
            }
        }

works perfect. I don't know why this method is private

OTHER TIPS

I don't believe that this is natively supported. I thought of a 'messy' way of doing it manually:

You can use the NumberPicker's scrollBy(int x, int y) function called iteratively to make the effect of the animation.

Some things to take into account:

  • scrollBy(x,y) works with pixels. As Android has got all that different screen densities, what you should do is first guess (I'd do it by trial and error) the 'dp' distance that corresponds to scrolling to a consecutive value, and convert that to pixels in order to use it.
  • The first parameter of scrollBy(x,y) should take a value of 0, in case it is not obvious

I haven't made animations myself in Android, but there is a very good API since Honeycomb which probably makes it easy to accomplish this.

I am sorry, but this is the easiest I could think of!

EDIT: To convert from 'dp' to pixels:

private float pxFromDp(float dp)
{
     return dp * this.getContext().getResources().getDisplayMetrics().density;
}

What you'll have to do is test calling scrollBy(0, pxFromDp(dp)) with different 'dp' values, until you get the exact one that moves the NumberPicker up one number. Once you get that value, you can create your animation method that when moving up X numbers will scroll X times this dp distance.

Please ask again if you don't understand it completely :)

Thank @passsy. Inspired his answer: This solution supports multiple increment/decrement steps. Also, can be executed with a start delay and for multiple numberPickers (without extra coding).

class IncreaseValue {
    private int counter = 0;
    private final NumberPicker picker;
    private final int incrementValue;
    private final boolean increment;

    private final Handler handler = new Handler();
    private final Runnable fire = new Runnable() { @Override public void run() { fire(); } };

    /*public*/ IncreaseValue(final NumberPicker picker, final int incrementValue) {
        this.picker = picker;
        if (incrementValue > 0) {
            increment = true;
            this.incrementValue = incrementValue;
        } else {
            increment = false;
            this.incrementValue = -incrementValue;
        }
    }

    /*public*/ void run(final int startDelay) {
        handler.postDelayed(fire, startDelay);  // This will execute the runnable passed to it (fire)
        // after [startDelay in milliseconds], ASYNCHRONOUSLY.
    }

    private void fire() {
        ++counter;
        if (counter > incrementValue) return;

        try {
            // refelction call for
            // picker.changeValueByOne(true);
            final Method method = picker.getClass().getDeclaredMethod("changeValueByOne", boolean.class);
            method.setAccessible(true);
            method.invoke(picker, increment);

        } catch (final NoSuchMethodException | InvocationTargetException | 
                IllegalAccessException | IllegalArgumentException e) {
            Log.d(TAG, "...", e);
        }

        handler.postDelayed(fire, 120);  // This will execute the runnable passed to it (fire)
        // after 120 milliseconds, ASYNCHRONOUSLY. Customize this value if necessary.
    }
}

The usage:

// Suppose picker1 as a NumberPicker
IncreaseValue increasePicker1 = new IncreaseValue(picker1, 10);  // So picker1 will be increased 10 steps.
increasePicker1.run(0);  // Increase immediately. If you want to run it from onCreate(), onStart() or ... of your activity, you will probably need to pass a positive value to it (e.g. 100 milliseconds).

There is private method inside NumberPicker, which is

changeCurrentByOne(boolean increment)

You can make it public and use it. You can see this method in action when pressing UP or DOWN arrow of NumberPicker. Probably it's not what you are looking for since this method does not give much control of animation, but it's definitelly better than setValue. Without any animation setValue looks terrible to user.

Thanks to @passy for the answer. Can be further simplified with a Kotlin Extension function:

fun NumberPicker.animateChange(isIncrement: Boolean) {
    Handler().post {
        try {
            javaClass.getDeclaredMethod("changeValueByOne", Boolean::class.javaPrimitiveType).also { function ->
                function.isAccessible = true
                function.invoke(this, isIncrement)
            }
        } catch (e: Exception) {
            Log.e(javaClass.simpleName, e.message)
        }
    }
}

Call

accessibilityNodeProvider.performAction(View.NO_ID, AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD, null)

to increase by 1

or

accessibilityNodeProvider.performAction(View.NO_ID, AccessibilityNodeInfo.ACTION_SCROLL_FORWARD, null)

to decrease by 1

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top