2

I want to change selected field of text color using setTextColor. But Android Studio gives me this error. What should I do? Min SDK is 21. This is code of my CustomNumberPicker class:

import android.annotation.TargetApi
import android.content.Context
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.NumberPicker
import android.widget.NumberPicker.OnScrollListener
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.widget.TextViewCompat
import ir.partsoftware.cup.R
import timber.log.Timber

class CustomNumberPicker : NumberPicker {

    constructor(context: Context?) : super(context) {
        init()
    }

    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
        init()
    }

    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        init()
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int,
        defStyleRes: Int
    ) : super(context, attrs, defStyleAttr, defStyleRes) {
        init()
    }

    private fun init() {
        setDividerColor(ContextCompat.getColor(context, R.color.color_secondary))
        setNumberPickerTextColor(this, ContextCompat.getColor(context, R.color.color_secondary))
        this.setOnValueChangedListener { picker, oldVal, newVal ->
            setNumberPickerTextColor(this, ContextCompat.getColor(context, R.color.color_secondary))
        }
        this.setOnScrollListener { numberPicker, scrollState ->
            setNumberPickerTextColor(this, ContextCompat.getColor(context, R.color.color_secondary))
        }
    }

    private fun setNumberPickerTextColor(numberPicker: NumberPicker, color: Int) {
        if (VERSION.SDK_INT >= VERSION_CODES.Q) {
            numberPicker.textColor = color
        } else {
            val count = numberPicker.childCount
            for (i in 0 until count) {
                val child = numberPicker.getChildAt(i)
                if (child is EditText) {
                    try {
                        child.setTextColor(color)
                        val fieldSelectorWheelPaint = numberPicker.javaClass.getDeclaredField("mSelectorWheelPaint")
                        val paint = fieldSelectorWheelPaint[numberPicker] as Paint
                        paint.color = color
                        fieldSelectorWheelPaint.isAccessible = true
                        numberPicker.invalidate()
                    } catch (ex: java.lang.Exception) {
                        // Ignore
                    }
                }
            }
        }
    }

    private fun setDividerColor(@ColorInt color: Int) {
        try {
            val fDividerDrawable =
                NumberPicker::class.java.getDeclaredField("mSelectionDivider")
            fDividerDrawable.isAccessible = true
            val d = fDividerDrawable[this] as Drawable
            DrawableCompat.setTint(d, color)
            d.invalidateSelf()
            postInvalidate()
        } catch (e: Exception) {
            Timber.d(e)
        }
    }

    override fun addView(
        child: View,
        index: Int,
        params: ViewGroup.LayoutParams
    ) {
        super.addView(child, index, params)
        updateView(child)
    }

    private fun updateView(view: View) {
        if (view is EditText) {
            try {
                TextViewCompat.setTextAppearance(view, R.style.TextAppearance_PartPay_NumPicker)
                val customFont: Typeface? = ResourcesCompat.getFont(context, R.font.iran_yekan)
                view.typeface = customFont
                //  setNumberPickerTextColor(ContextCompat.getColor(context, R.color.color_secondary))
            } catch (e: Exception) {
                Timber.d(e)
            }
        }
    }
}
MMG
  • 3,226
  • 5
  • 16
  • 43
  • Check some of the solutions at this answer: https://stackoverflow.com/questions/22962075/change-the-text-color-of-numberpicker – PerracoLabs Sep 27 '20 at 09:40
  • I have checked them before asking this question – MMG Sep 27 '20 at 09:52
  • Aren't working for you the solutions provided by Tapa Save or Eduardo Reis? In API below 29 the text color is only accessible via reflection. Therefore, the solution is an if / else condition, where if >= 29 use the setTextColor, and if below use reflection to access the field. – PerracoLabs Sep 27 '20 at 10:05

1 Answers1

1

Try the next code. Will use reflection when the API is not accesible:

public void setNumberPickerTextColor(final NumberPicker numberPicker, final int color){

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        numberPicker.setTextColor(color);
    }
    else {
        final int count = numberPicker.getChildCount();

        for (int i = 0; i < count; i++) {

            final View child = numberPicker.getChildAt(i);

            if (child instanceof EditText) {
                try {
                    ((EditText)child).setTextColor(color);
                    numberPicker.invalidate();

                    final Field fieldSelectorWheelPaint = numberPicker.getClass().getDeclaredField("mSelectorWheelPaint");
                    boolean     isAccessible            = fieldSelectorWheelPaint.isAccessible();
                    fieldSelectorWheelPaint.setAccessible(true);
                    final Paint paint = (Paint)fieldSelectorWheelPaint.get(numberPicker);

                    if (paint != null){
                        paint.setColor(color);
                        fieldSelectorWheelPaint.setAccessible(isAccessible);
                        numberPicker.invalidate();
                    }

                    final Field fieldSelectionDivider = numberPicker.getClass().getDeclaredField("mSelectionDivider");
                    isAccessible = fieldSelectionDivider.isAccessible();
                    fieldSelectionDivider.setAccessible(true);
                    fieldSelectionDivider.set(numberPicker, null);
                    fieldSelectionDivider.setAccessible(isAccessible);
                    numberPicker.invalidate();
                }
                catch (Exception ex) {
                    // Ignore
                }
            }
        }
    }
}

You may call this method the first time you get a reference to the control, and in addition if the color doesn't persist after scrolling, then hook listener such as next:

numberPicker.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
    @Override
    public void onValueChange(final NumberPicker picker, final int oldVal, final int newVal) {
        setNumberPickerTextColor(numberPicker, Color.RED);
    }
});

Or alternatively you can also hook a scroll listener, although the above setOnValueChangedListener example is more optimal, as it will only perform the update when the value is changed. To improve the next scroll method you could check if the scrollState is in an idle state, so it is only called when scrolling ends:

numberPicker.setOnScrollListener(new NumberPicker.OnScrollListener() {
    @Override
    public void onScrollStateChange(final NumberPicker numberPicker, final int scrollState) {
        setNumberPickerTextColor(numberPicker, Color.RED);
    }
});

UPDATE: The next section is specific only to your updated question code.

The problem is that you are extending a NumberPicker class, in such case you need to use the getDeclaredField on the super class. My above answer can be used only when not extending the NumberPicker class. In addition you've placed the isAccesible in the wrong line, it needs to be a bit before to make it accesible.

Next is the correction to your code which can be used perfectly when extending a NumberPicker class. You can see that getDeclaredField is preceded by superclass, and isAccessible is at the correct position:

private fun setNumberPickerTextColor(numberPicker: NumberPicker, color: Int) {
    if (VERSION.SDK_INT >= VERSION_CODES.Q) {
        numberPicker.textColor = color
    } else {
        val count = numberPicker.childCount
        for (i in 0 until count) {
            val child = numberPicker.getChildAt(i)
            if (child is EditText) {
                try {
                    child.setTextColor(color)
                    val fieldSelectorWheelPaint = numberPicker.javaClass.superclass.getDeclaredField("mSelectorWheelPaint")
                    fieldSelectorWheelPaint.isAccessible = true
                    val paint = fieldSelectorWheelPaint[numberPicker] as Paint
                    paint.color = color
                    numberPicker.invalidate()
                } catch (ex: java.lang.Exception) {
                    // Ignore
                }
            }
        }
    }
}
PerracoLabs
  • 16,449
  • 15
  • 74
  • 127
  • Have you tested this code? Problem of this code is that when we scroll numberpicker, color will come back to default one. First time numberpicker is shown, it is what I want but after scrolling not. – MMG Sep 27 '20 at 12:08
  • Hook a listener such as numberPicker.setOnValueChangedListener and call the answer's method whenever the NumberPicker's value changes. Alternatively you can also set also a scroll listener such as numberPicker.setOnScrollListener, and again also call the answer's method but only when scrollState becomes SCROLL_STATE_IDLE. From both options the most optimal would be to use the setOnValueChangedListener variant. – PerracoLabs Sep 27 '20 at 12:17
  • I put onscroll listener in my code and when I scroll on numberpicker, it is detected correctly. In that listener I invoke again the function to change selected text color but it doesn't work and after scrolling color come back to its default. If you know answer of this question and the bounty question that I sent you its link, please answer faster because the bounty will be expired soon. – MMG Sep 27 '20 at 19:47
  • Have you tried to use both listeners at the same time? setOnValueChangedListener and setOnScrollListener, I've tested the code and it works correctly. – PerracoLabs Sep 27 '20 at 19:49
  • The only way possible the color is going back to the default or changed, is if somewhere else in your code you call setTextColor. Make sure that you don't call this method anywhere else. and use only setNumberPickerTextColor all the time everywhere to set the color. – PerracoLabs Sep 27 '20 at 20:04
  • It seems that color of unselected fields is changed too. But if we want to just change selected field color? – MMG Sep 28 '20 at 07:46
  • What I want is what is in image of this answer https://stackoverflow.com/a/56678929/12478830 – MMG Sep 28 '20 at 09:41
  • If you check the NumberPicker's source you'll see this isn't possible. The reason is it renders all scrolling and center numbers as the same thing, with the same paint instance and 1 color, making no differentiation between the center or other numbers. The center color can be different for a short moment when NumberPicker is in editing. In such case its EditText control appears on top of the NumberPicker which can have a different color, but as soon as focus is lost when scrolling, or touching outside, the EditText is hidden, and show again the underneath numbers having all the same color – PerracoLabs Sep 28 '20 at 10:03
  • Check the onDraw method at the next link, you will see is impossible to achieve what you need. All the text, even the center, is rendered where the comment "draw the selector wheel", making no differentiation between the center and the rest. The screenshot from the other answer sets the NumberPicker in edit mode all the time, but if it loses focus, it will also loose the center color. Is just impossible. A solution is that you take the next code and create from it a new control: https://android.googlesource.com/platform/frameworks/base/+/0e2d281/core/java/android/widget/NumberPicker.java – PerracoLabs Sep 28 '20 at 10:10
  • Why is it impossible? What if API is greater than 29? – MMG Sep 28 '20 at 10:11
  • For newer APIs it may be using a new implementation not available in older implementations. Newer APIs means sometimes a complete rewrite of code very different from old APIs, and functionality can't be ported or emulated. The code is at API level, this means that older APIs don't have such code to make it possible. Check the source code of NumberPicker, is quite clear is impossible. The best option is you write a new control from scratch without extending the NumberPicker, you may find some in GitHub – PerracoLabs Sep 28 '20 at 10:16